mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-12-06 06:36:25 +00:00
Compare commits
434 Commits
feat/goqua
...
v4.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 266caa3f02 | |||
| ea98598399 | |||
| 83110374f8 | |||
| 1f16271354 | |||
| 0c387fc19a | |||
| 7d423bb049 | |||
| c3bebdec01 | |||
| d26934b602 | |||
| e16fdc1036 | |||
| 0008aa95c2 | |||
| a8d29b5b26 | |||
| 61a8b07f4b | |||
| d3ec0cab9b | |||
| 5e054e12f7 | |||
| e8896b7787 | |||
| 0cf7b1369b | |||
| 7428147100 | |||
| 88f3f7a9ae | |||
| 2ff507f641 | |||
| 3de751c880 | |||
| 9a9e43269d | |||
| 12276ed034 | |||
| 22b68f4dc7 | |||
| 80e928ee43 | |||
| bb3e57ff2a | |||
| 78247acba7 | |||
| e4f00d34bc | |||
| 81d4a13735 | |||
| 5d430f8ee8 | |||
| aba6c873df | |||
| d140a6e392 | |||
| f7603db588 | |||
| 87754ae928 | |||
| ecbf1f5db5 | |||
| 8ddf22d661 | |||
| 13df17544c | |||
| 39ce601344 | |||
| db6670f699 | |||
| bd7991942a | |||
| da594d6df1 | |||
| f6bb77a6a5 | |||
| ea979d9663 | |||
| 646df0f393 | |||
| d24d18ea8e | |||
| 9740a5d0d4 | |||
| 034a554bf4 | |||
| fb1c006cd9 | |||
| 928bfe7876 | |||
| 6485c733bb | |||
| b44bdb8a75 | |||
| 71f56699c8 | |||
| 66fa07f341 | |||
| 2c8467090e | |||
| a18d5d0532 | |||
| c5b0a76982 | |||
| f352085f62 | |||
| 5374666ac9 | |||
| 411bbef65c | |||
| f798f2c025 | |||
| 44c88a885f | |||
| b08bfeceb6 | |||
| be8bf53cc6 | |||
| fee6032e78 | |||
| 64eb70d292 | |||
| dc7f7feab1 | |||
| 8afbc63b85 | |||
| c942794b89 | |||
| e58b5a063f | |||
| dee3af3016 | |||
| 9f42c29714 | |||
| 95bc5b9f7c | |||
| 1cd3704bc3 | |||
| 8bbccd42d5 | |||
| f89ce7a965 | |||
| 7905edaf24 | |||
| 851baa030c | |||
| 567d2ac686 | |||
| 71bf334ac4 | |||
| 2df874e786 | |||
| fe9aa865f9 | |||
| 1e8316e16d | |||
| 380c80bbaf | |||
| a8fe8e2e13 | |||
| 22d0d064f7 | |||
| a5c7aef3b8 | |||
| e1f889f862 | |||
| c6f12ab2a8 | |||
| 31d8dcd6a8 | |||
| 1f3a985d3a | |||
| 6937a982d4 | |||
| 44bb88910f | |||
| 115b9fa4b3 | |||
| b6f9c050e1 | |||
| cbb05ac977 | |||
| f1d72cb480 | |||
| 3a5d6ed2cd | |||
| c15dcb02ec | |||
| 0d91001376 | |||
| 6143125f7a | |||
| ee0f703120 | |||
| 34393bf050 | |||
| 5a461bca7d | |||
| 5fedce71a0 | |||
| c9663ff14f | |||
| 18e301f26a | |||
| 35e37bbe76 | |||
| 5997921eb9 | |||
| e7bedd6a29 | |||
| 7194dcb2c7 | |||
| d62bdfc637 | |||
| ec6b90b33c | |||
| 0c0037416a | |||
| ad9d1ee430 | |||
| d7e5b8b916 | |||
| 3e44d38a90 | |||
| 9493531eaa | |||
| 64031668c1 | |||
| b6d122e449 | |||
| 19f26c6d91 | |||
| 4108434788 | |||
| 01d7f62c36 | |||
| 5cffeea4fd | |||
| e0fb29bd80 | |||
| ad9a59f894 | |||
| ddad768cd8 | |||
| 8ee4446b30 | |||
| 5f936d36b1 | |||
| 08f3e9c06b | |||
| 25b7903c37 | |||
| c6dd7203bb | |||
| 08c7ca99b6 | |||
| 8f7320c298 | |||
| 92bfbf662b | |||
| 44e7323720 | |||
| 78a3ae8aeb | |||
| 041abb732d | |||
| 9ee07794a8 | |||
| 7adfef9f36 | |||
| a3ec224cf0 | |||
| 6d4a6ee52a | |||
| 0add402434 | |||
| 6933aecfa4 | |||
| 93decb02af | |||
| c0acf1c1a0 | |||
| a6c3ab33b1 | |||
| 079a2cdbe3 | |||
| d9022fde9f | |||
| 830a518b86 | |||
| fef04409af | |||
| 4bc54d350b | |||
| e3cc80d32a | |||
| 411e05e998 | |||
| c319f6117a | |||
| a7fd96800a | |||
| a09e229711 | |||
| 7abb66b86f | |||
| f1707db5fb | |||
| 35e1cc7253 | |||
| e8b4a26eda | |||
| 93f93f0186 | |||
| 7c5de3c131 | |||
| a2a3134523 | |||
| 3821950e49 | |||
| d52cc045e0 | |||
| 158058b720 | |||
| df6f9ea71d | |||
| f4464578c0 | |||
| c48ee975c6 | |||
| e60e2306b7 | |||
| af422e62e1 | |||
| 952beccafc | |||
| 06171ae638 | |||
| b3a341847e | |||
| 0d325f2c73 | |||
| 577f3f768d | |||
| 15d479f1eb | |||
| 2b93076146 | |||
| 68c83d8a5d | |||
| 239ad9a4dc | |||
| 5f8d0d1b99 | |||
| 5827a18866 | |||
| 633db89031 | |||
| 1335ae13e8 | |||
| 115d52977d | |||
| 3bb36f5e78 | |||
| 98e9ba0fa7 | |||
| 35a69edfa2 | |||
| 7df1a295f3 | |||
| 85fbd37434 | |||
| 07f0862219 | |||
| bc99408652 | |||
| 461dad2724 | |||
| 6cf76f6535 | |||
| 33a5893da1 | |||
| c14c0a6af5 | |||
| fc7926c2cc | |||
| faf8832572 | |||
| d047e5d48a | |||
| 7baa586738 | |||
| 4c7e335ef4 | |||
| df8a1d3b26 | |||
| 6a1fff227e | |||
| 4810d6cc5c | |||
| 1b73beccf1 | |||
| ff8a791a51 | |||
| 98ead6ac69 | |||
| 8b6741641c | |||
| b035ad07ec | |||
| 25fc5d5835 | |||
| c1cdcddf41 | |||
| 9a5142ced3 | |||
| 586b7900bb | |||
| 0ccb03f004 | |||
| 90676ff8a4 | |||
| 2cacd94f41 | |||
| de3013eebf | |||
| 1c71258984 | |||
| 00831fdb61 | |||
| f5c629cb8a | |||
| 9531795066 | |||
| 71fe10efaf | |||
| b042b4cf60 | |||
| f4980cefde | |||
| 25901cef45 | |||
| d71cd625d0 | |||
| 43350ee1fd | |||
| ae1dee9d51 | |||
| ff2d077a7f | |||
| a383de971a | |||
| 800fa13071 | |||
| 490c68a9f5 | |||
| 1ba03ba909 | |||
| 346750931d | |||
| e612869027 | |||
| 2673ddaf13 | |||
| a3172c7918 | |||
| 8269d80620 | |||
| d1158cab05 | |||
| 09430e56b8 | |||
| 51d3684fcc | |||
| 2968ca3562 | |||
| ee3d8916ed | |||
| acedb77c07 | |||
| 32b1681573 | |||
| 5389e1b783 | |||
| f54a876636 | |||
| 2afed432f7 | |||
| b687d8ea95 | |||
| 2594afc60f | |||
| f5be4a8b99 | |||
| a8b0eeb973 | |||
| ff5ecb474f | |||
| 0a0939fa3d | |||
| 19485a110a | |||
| 79dc4e5f33 | |||
| 7f6721147a | |||
| 20bf6851c0 | |||
| 7adbb5d299 | |||
| 2a3d5de54b | |||
| 18ff6fe71b | |||
| a278e3a565 | |||
| 9f003189e9 | |||
| 050b420f9a | |||
| 2b59a35bf3 | |||
| 7f20a3f36a | |||
| 784289a792 | |||
| d79a73d311 | |||
| 13c0430c93 | |||
| 1e4081a03f | |||
| 275cc70e96 | |||
| 3fccbae676 | |||
| 1fb3088f0e | |||
| 81131edf2d | |||
| 973685ec08 | |||
| 6139deb175 | |||
| 93daed8ec8 | |||
| 08c34a18f2 | |||
| 682dd1093f | |||
| 6806d1f242 | |||
| 394fe4871f | |||
| edc1619962 | |||
| 970d613285 | |||
| 4167704f85 | |||
| 1e0ff4a950 | |||
| 4993d34835 | |||
| 3f9446d46f | |||
| 030a4e0e86 | |||
| d27cf2afc8 | |||
| 5b27eab680 | |||
| 345eabafb2 | |||
| c635fc00c3 | |||
| 2877083ebe | |||
| bb29b7e7f7 | |||
| 7aca2b2d6d | |||
| fe5ba9a84a | |||
| 2067a58c70 | |||
| 8b2c0f732f | |||
| fbd76594ea | |||
| a055dfac5b | |||
| e772a798f7 | |||
| ed9c4ebb68 | |||
| 0439e1f37a | |||
| eb4f88bc60 | |||
| 666477e448 | |||
| 6787400056 | |||
| 2ecda09ee4 | |||
| 0bd497279d | |||
| 1023cf0120 | |||
| f4dc4c315d | |||
| 8b92d0525f | |||
| 530811b699 | |||
| cee7ca2ca0 | |||
| c26a95ed60 | |||
| b43b6d6f75 | |||
| c969908ff5 | |||
| a5638203a6 | |||
| 042cc018cb | |||
| 42d2b8009c | |||
| 5264214eb3 | |||
| bba1fd964d | |||
| 5821a79af9 | |||
| 9dde2475fc | |||
| f90d2d2f04 | |||
| b4ba255afc | |||
| 1abee46f6d | |||
| 0fbfd5731d | |||
| 6265f9bc2c | |||
| 8c910fa532 | |||
| 08d2bb2fd5 | |||
| 885b784f92 | |||
| 7c56ec8285 | |||
| b21fc9db25 | |||
| e06989f2ae | |||
| fab2784e16 | |||
| b323736774 | |||
| a0c1fdbd74 | |||
| 2db6255fae | |||
| 898e7b272e | |||
| f0e6ee5835 | |||
| a01ce5c11c | |||
| c7e6114480 | |||
| e3908da7a9 | |||
| 93608c9549 | |||
| ecc2b70e43 | |||
| 92a3c2945c | |||
| 0be1bf4f15 | |||
| 65b0b22b01 | |||
| 7b4b572802 | |||
| 049474e4bd | |||
| 5c11372543 | |||
| de0eb0b4e9 | |||
| e60a1e5a25 | |||
| 27ae6b512b | |||
| 5f89c6e498 | |||
| b760287ca2 | |||
| e80543e0a2 | |||
| af6436c3d8 | |||
| f65e4bc417 | |||
| 68c28ed358 | |||
| fed94eab1b | |||
| 1dbee1d79f | |||
| 09b146928c | |||
| 983b558510 | |||
| 9fd7ca35f1 | |||
| e886fbcc5f | |||
| 853bfd5f9b | |||
| 6ba0786608 | |||
| b33b428d3b | |||
| 19c5efaed0 | |||
|
|
53285059c5 | ||
| a8e4b74fba | |||
| 2374c2d22e | |||
| 1df8d6589a | |||
| 90498eb117 | |||
| 0cc6274894 | |||
| 085d337fb7 | |||
| a56c06ca33 | |||
| d026f9b418 | |||
| 7803f8f11a | |||
| ec90862262 | |||
| 5efcfb8b61 | |||
| 7193a599b7 | |||
| 6ec387e724 | |||
| c3e8f87562 | |||
| ac846ad351 | |||
| 647a4c3782 | |||
| 1901c908ff | |||
| 797ea13982 | |||
| 6b006e78e5 | |||
| 733d6c8c7b | |||
| d3d59443fa | |||
| a74fbce48e | |||
| cecf7b0f74 | |||
| 8a862c86a5 | |||
| e6a9e2c7da | |||
| b147ee8850 | |||
| 2d8bd207ed | |||
| 3a125263b7 | |||
| a6a26cdf8d | |||
| ec1184d5c9 | |||
| ae518cafe5 | |||
| 3ae167bd16 | |||
| 82c342c7db | |||
| 0e10f29cd2 | |||
| 9c03bac804 | |||
| 6ff41c55fe | |||
| 992b64df8a | |||
| 53ac4a2050 | |||
|
|
d9f4a6ff8d | ||
| 521a68b4c7 | |||
| 8e74413e18 | |||
|
|
5f7ef2a18e | ||
|
|
fa969fa702 | ||
|
|
4aaba75c18 | ||
|
|
b98df08f44 | ||
|
|
4332bfee9b | ||
|
|
4430450431 | ||
|
|
aea6253094 | ||
|
|
37acfa1eec | ||
|
|
0955da489c | ||
|
|
2544f8f333 | ||
|
|
409a613e43 | ||
| 14da738cc8 | |||
| 7a8cc242ae | |||
| c0e6012d70 | |||
| b6df0ba2b1 | |||
| b9c1c766d6 | |||
| 802a872880 | |||
| 415b5b45b9 | |||
| dd9e0b4fc7 | |||
| 6c9a0ea576 | |||
| 995f50cc21 | |||
| 2a491ded00 | |||
| 0517f85d76 |
55
.env.example
55
.env.example
@@ -1,23 +1,60 @@
|
||||
# vi: ft=sh
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
|
||||
# Useful config options
|
||||
|
||||
# Library root can either be an absolute path or a relative path to your docker-compose.yml file.
|
||||
LIBRARY_ROOT=./video
|
||||
CACHE_ROOT=/tmp/kyoo_cache
|
||||
LIBRARY_LANGUAGES=en
|
||||
|
||||
# A pattern (regex) to ignore video files.
|
||||
LIBRARY_IGNORE_PATTERN=.*/[dD]ownloads?/.*
|
||||
LIBRARY_IGNORE_PATTERN=".*/[dD]ownloads?/.*"
|
||||
|
||||
# If this is true, new accounts wont have any permissions before you approve them in your admin dashboard.
|
||||
REQUIRE_ACCOUNT_VERIFICATION=true
|
||||
# Specify permissions of guest accounts, default is no permissions.
|
||||
UNLOGGED_PERMISSIONS=
|
||||
# but you can allow anyone to use your instance without account by doing:
|
||||
# UNLOGGED_PERMISSIONS=overall.read,overall.play
|
||||
# You can specify this to allow guests users to see your collection without behing able to play videos for example:
|
||||
# UNLOGGED_PERMISSIONS=overall.read
|
||||
|
||||
# Specify permissions of new accounts.
|
||||
DEFAULT_PERMISSIONS=overall.read,overall.play
|
||||
|
||||
# Hardware transcoding (equivalent of --profile docker compose option).
|
||||
COMPOSE_PROFILES= # vaapi or qsv or nvidia
|
||||
# the preset used during transcode. faster means worst quality, you can probably use a slower preset with hwaccels
|
||||
# warning: using vaapi hwaccel disable presets (they are not supported).
|
||||
GOCODER_PRESET=fast
|
||||
|
||||
|
||||
# The following two values should be set to a random sequence of characters.
|
||||
# You MUST change thoses when installing kyoo (for security)
|
||||
AUTHENTICATION_SECRET=4c@mraGB!KRfF@kpS8739y9FcHemKxBsqqxLbdR?
|
||||
AUTHENTICATION_SECRET="4c@mraGB!KRfF@kpS8739y9FcHemKxBsqqxLbdR?"
|
||||
# You can input multiple api keys separated by a ,
|
||||
KYOO_APIKEYS=t7H5!@4iMNsAaSJQ49pat4jprJgTcF656if#J3
|
||||
|
||||
DEFAULT_PERMISSIONS=overall.read
|
||||
UNLOGGED_PERMISSIONS=overall.read
|
||||
|
||||
THEMOVIEDB_APIKEY=
|
||||
PUBLIC_BACK_URL=http://localhost:5000
|
||||
|
||||
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
|
||||
PUBLIC_URL=http://localhost:5000
|
||||
|
||||
# Use a builtin oidc service (google, discord, trakt, or simkl):
|
||||
# When you create a client_id, secret combo you may be asked for a redirect url. You need to specify https://YOUR-PUBLIC-URL/api/auth/logged/YOUR-SERVICE-NAME
|
||||
OIDC_DISCORD_CLIENTID=
|
||||
OIDC_DISCORD_SECRET=
|
||||
# Or add your custom one:
|
||||
OIDC_SERVICE_NAME=YourPrettyName
|
||||
OIDC_SERVICE_LOGO=https://url-of-your-logo.com
|
||||
OIDC_SERVICE_CLIENTID=
|
||||
OIDC_SERVICE_SECRET=
|
||||
OIDC_SERVICE_AUTHORIZATION=https://url-of-the-authorization-endpoint-of-the-oidc-service.com/auth
|
||||
OIDC_SERVICE_TOKEN=https://url-of-the-token-endpoint-of-the-oidc-service.com/token
|
||||
OIDC_SERVICE_PROFILE=https://url-of-the-profile-endpoint-of-the-oidc-service.com/userinfo
|
||||
OIDC_SERVICE_SCOPE="the list of scopes space separeted like email identity"
|
||||
# on the previous list, service is the internal name of your service, you can add as many as you want.
|
||||
|
||||
|
||||
# Following options are optional and only useful for debugging.
|
||||
@@ -37,4 +74,6 @@ POSTGRES_PORT=5432
|
||||
MEILI_HOST="http://meilisearch:7700"
|
||||
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"
|
||||
|
||||
# vi: ft=sh
|
||||
RABBITMQ_HOST=rabbitmq
|
||||
RABBITMQ_DEFAULT_USER=kyoo
|
||||
RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
7e6e56a366babe17e7891a5897180efbf93c00c5
|
||||
a5638203a6ecb9f372a5a61e1c8fd443bf3a17fe
|
||||
18e301f26acd7f2e97eac26c5f48377fa13956f5
|
||||
|
||||
64
.gitattributes
vendored
64
.gitattributes
vendored
@@ -1,63 +1 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
||||
*.Designer.cs linguist-generated=true
|
||||
|
||||
22
.github/workflows/coding-style.yml
vendored
22
.github/workflows/coding-style.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
run:
|
||||
working-directory: ./back
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check coding style
|
||||
run: |
|
||||
@@ -23,10 +23,10 @@ jobs:
|
||||
run:
|
||||
working-directory: ./front
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: yarn
|
||||
@@ -39,18 +39,14 @@ jobs:
|
||||
run: yarn lint && yarn format
|
||||
|
||||
scanner:
|
||||
name: "Lint scanner"
|
||||
name: "Lint scanner/autosync"
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./scanner
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run black
|
||||
run: |
|
||||
pip3 install black-with-tabs
|
||||
black . --check
|
||||
- uses: chartboost/ruff-action@v1
|
||||
with:
|
||||
args: format --check
|
||||
|
||||
transcoder:
|
||||
name: "Lint transcoder"
|
||||
@@ -59,7 +55,7 @@ jobs:
|
||||
run:
|
||||
working-directory: ./transcoder
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run go fmt
|
||||
run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi
|
||||
|
||||
47
.github/workflows/docker.yml
vendored
47
.github/workflows/docker.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -16,25 +17,39 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- context: ./back
|
||||
dockerfile: Dockerfile
|
||||
label: back
|
||||
image: zoriya/kyoo_back
|
||||
|
||||
- context: ./back
|
||||
dockerfile: Dockerfile.migrations
|
||||
label: migrations
|
||||
image: zoriya/kyoo_migrations
|
||||
|
||||
- context: ./front
|
||||
dockerfile: Dockerfile
|
||||
label: front
|
||||
image: zoriya/kyoo_front
|
||||
|
||||
- context: ./scanner
|
||||
dockerfile: Dockerfile
|
||||
label: scanner
|
||||
image: zoriya/kyoo_scanner
|
||||
|
||||
- context: ./autosync
|
||||
dockerfile: Dockerfile
|
||||
label: autosync
|
||||
image: zoriya/kyoo_autosync
|
||||
|
||||
- context: ./transcoder
|
||||
dockerfile: Dockerfile
|
||||
label: transcoder
|
||||
image: zoriya/kyoo_transcoder
|
||||
name: Build ${{matrix.label}}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dorny/paths-filter@v2
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
@@ -43,46 +58,50 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{matrix.image}}
|
||||
tags: |
|
||||
type=edge
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Check if a package should be pushed
|
||||
run: |
|
||||
echo "SHOULD_PUSH=$([ "${GITHUB_REF##*/}" == "master" ] || [ "${GITHUB_REF##*/}" == "next" ] && echo "true" || echo "false")" >> $GITHUB_ENV
|
||||
echo "SHOULD_PUSH=$([ "${GITHUB_REF##*/}" == "master" ] || [ "${GITHUB_REF##*/}" == "next" ] || [ "${GITHUB_REF_TYPE}" == "tag" ] && echo "true" || echo "false")" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: ${{env.SHOULD_PUSH}}
|
||||
uses: docker/login-action@v1
|
||||
if: env.SHOULD_PUSH == 'true'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
if: steps.filter.outputs.should_run == 'true'
|
||||
uses: docker/build-push-action@v5
|
||||
if: steps.filter.outputs.should_run == 'true' || github.event_name == 'workflow_dispatch' || startsWith(github.event.ref, 'refs/tags/v')
|
||||
with:
|
||||
context: ${{matrix.context}}
|
||||
file: ${{matrix.context}}/${{matrix.dockerfile}}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=0.0.0
|
||||
push: ${{env.SHOULD_PUSH}}
|
||||
tags: ${{steps.meta.outputs.tags}}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Sync README.MD
|
||||
if: ${{env.SHOULD_PUSH}}
|
||||
if: env.SHOULD_PUSH == 'true'
|
||||
uses: ms-jpq/sync-dockerhub-readme@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
||||
6
.github/workflows/native-build.yml
vendored
6
.github/workflows/native-build.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
working-directory: ./front
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: |
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: yarn
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
- name: Download APK Asset
|
||||
run: wget -O kyoo.apk ${{ steps.url.outputs.assetUrl }}
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: kyoo.apk
|
||||
path: ./front/kyoo.apk
|
||||
|
||||
4
.github/workflows/native-update.yml
vendored
4
.github/workflows/native-update.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
working-directory: ./front
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: |
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: yarn
|
||||
|
||||
8
.github/workflows/robot.yml
vendored
8
.github/workflows/robot.yml
vendored
@@ -12,10 +12,8 @@ jobs:
|
||||
name: Run Robot Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Pull images
|
||||
run: |
|
||||
cp .env.example .env
|
||||
@@ -45,7 +43,7 @@ jobs:
|
||||
if: failure()
|
||||
run: docker compose logs
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: results
|
||||
path: out
|
||||
|
||||
60
.github/workflows/tests.yml
vendored
60
.github/workflows/tests.yml
vendored
@@ -1,60 +0,0 @@
|
||||
name: Testing
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Back tests
|
||||
runs-on: ubuntu-latest
|
||||
container: mcr.microsoft.com/dotnet/sdk:7.0
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd back
|
||||
dotnet build '-p:SkipTranscoder=true' -p:CopyLocalLockFileAssemblies=true
|
||||
cp ./out/bin/Kyoo.Abstractions/Debug/net7.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll ./tests/Kyoo.Tests/bin/Debug/net7.0/
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
cd back
|
||||
dotnet test --no-build '-p:CollectCoverage=true;CoverletOutputFormat=opencover' --logger "trx;LogFileName=TestOutputResults.xml"
|
||||
env:
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
||||
- name: Sanitize coverage output
|
||||
if: ${{ always() }}
|
||||
run: sed -i "s'$(pwd)/back'.'" back/tests/Kyoo.Tests/coverage.opencover.xml
|
||||
|
||||
- name: Upload tests results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: results.xml
|
||||
path: "**/TestOutputResults.xml"
|
||||
|
||||
- name: Upload coverage report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: coverage.xml
|
||||
path: "**/coverage.opencover.xml"
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
||||
[submodule "transcoder"]
|
||||
path = src/Kyoo.Transcoder
|
||||
url = ../Kyoo.Transcoder.git
|
||||
branch = master
|
||||
@@ -1,3 +1,14 @@
|
||||
# Installing TLDR
|
||||
|
||||
1. Install docker & docker-compose
|
||||
2. Download the
|
||||
[`docker-compose.yml`](https://raw.githubusercontent.com/zoriya/Kyoo/master/docker-compose.prod.yml),
|
||||
[`nginx.conf.template`](https://raw.githubusercontent.com/zoriya/Kyoo/master/nginx.conf.template) and
|
||||
[`.env`](https://raw.githubusercontent.com/zoriya/Kyoo/master/.env.example) files
|
||||
3. Fill the `.env` file with your configuration options (and an API Key from [themoviedb.org](https://www.themoviedb.org/))
|
||||
4. Look at [Hardware Acceleration section](#Hardware-Acceleration) if you need it
|
||||
5. Run `docker compose up -d` and see kyoo at `http://localhost:8901`
|
||||
|
||||
# Installing
|
||||
|
||||
To install Kyoo, you need docker and docker-compose. Those can be installed from here for
|
||||
@@ -24,21 +35,13 @@ To retrieve metadata, Kyoo will need to communicate with an external service. Fo
|
||||
For this purpose, you will need to get an API Key. For that, go to [themoviedb.org](https://www.themoviedb.org/) and create an account, then
|
||||
go [here](https://www.themoviedb.org/settings/api) and copy the `API Key (v3 auth)`, paste it after the `THEMOVIEDB_APIKEY=` on the `.env` file.
|
||||
|
||||
If you need hardware acceleration, look at [Hardware Acceleration section](#Hardware-Acceleration) if you need it
|
||||
|
||||
The next and last step is actually starting Kyoo. To do that, open a terminal in the same directory as the 3 configurations files
|
||||
and run `docker-compose up -d`.
|
||||
|
||||
Congratulation, everything is now ready to use Kyoo. You can navigate to `http://localhost:8901` on a web browser to see your instance of Kyoo.
|
||||
|
||||
# Installing TLDR
|
||||
|
||||
1. Install docker & docker-compose
|
||||
2. Download the
|
||||
[`docker-compose.yml`](https://raw.githubusercontent.com/zoriya/Kyoo/master/docker-compose.prod.yml),
|
||||
[`nginx.conf.template`](https://raw.githubusercontent.com/zoriya/Kyoo/master/nginx.conf.template) and
|
||||
[`.env`](https://raw.githubusercontent.com/zoriya/Kyoo/master/.env.example) files
|
||||
3. Fill the `.env` file with your configuration options (and an API Key from [themoviedb.org](https://www.themoviedb.org/))
|
||||
4. Run `docker-compose up -d`
|
||||
|
||||
# Updating
|
||||
|
||||
Updating Kyoo is exactly the same as installing it. Get an updated version of the `docker-compose.yml` and `nginx.conf.template` files and
|
||||
@@ -54,3 +57,44 @@ TLDR: `docker run -d --name watchtower -e WATCHTOWER_CLEANUP=true -e WATCHTOWER_
|
||||
|
||||
To uninstall Kyoo, you need to open a terminal in the configuration's directory and run `docker-compose down`. This will
|
||||
stop Kyoo's services. You can then remove the configuration files.
|
||||
|
||||
# Hardware Acceleration
|
||||
|
||||
## VA-API (intel, amd)
|
||||
|
||||
First install necessary drivers on your system, when running `vainfo` you should have something like this:
|
||||
```
|
||||
libva info: VA-API version 1.20.0
|
||||
libva info: Trying to open /run/opengl-driver/lib/dri/iHD_drv_video.so
|
||||
libva info: Found init function __vaDriverInit_1_20
|
||||
libva info: va_openDriver() returns 0
|
||||
vainfo: VA-API version: 1.20 (libva 2.20.1)
|
||||
vainfo: Driver version: Intel iHD driver for Intel(R) Gen Graphics - 23.3.5 ()
|
||||
vainfo: Supported profile and entrypoints
|
||||
VAProfileH264Main : VAEntrypointVLD
|
||||
VAProfileH264Main : VAEntrypointEncSlice
|
||||
...Truncated...
|
||||
VAProfileHEVCSccMain444_10 : VAEntrypointVLD
|
||||
VAProfileHEVCSccMain444_10 : VAEntrypointEncSliceLP
|
||||
```
|
||||
Kyoo will default to use your primary card (located at `/dev/dri/renderD128`). If you need to specify a secondary one, you
|
||||
can use the `GOCODER_VAAPI_RENDERER` env-var to specify `/dev/dri/renderD129` or another one.
|
||||
|
||||
Then you can simply run kyoo using `docker compose --profile vaapi up -d` (notice the `--profile vaapi` added)
|
||||
You can also add `COMPOSE_PROFILES=vaapi` to your `.env` instead of adding the `--profile` flag.
|
||||
|
||||
## Nvidia
|
||||
|
||||
To enable nvidia hardware acceleration, first install necessary drivers on your system.
|
||||
|
||||
Then, install the [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html), you can simply
|
||||
follow the instructions on the official webpage or your distribution wiki.
|
||||
|
||||
To test if everything works, you can run `sudo docker run --rm --gpus all ubuntu nvidia-smi`. If your version of docker is older,
|
||||
you might need to add `--runtime nvidia` like so: `sudo docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi`
|
||||
|
||||
After that, you can now use `docker compose --profile nvidia up -d` to start kyoo with nvidia hardware acceleration (notice the `--profile nvidia` added).
|
||||
You can also add `COMPOSE_PROFILES=nvidia` to your `.env` instead of adding the `--profile` flag.
|
||||
|
||||
Note that most nvidia cards have an artificial limit on the number of encodes. You can confirm your card limit [here](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new).
|
||||
This limit can also be removed by applying an [unofficial patch](https://github.com/keylase/nvidia-patch) to you driver.
|
||||
|
||||
84
README.md
84
README.md
@@ -1,40 +1,64 @@
|
||||
# <img width="24px" src="./icons/icon-256x256.png" alt="Kyoo"> Kyoo
|
||||
# <img width="24px" src="./icons/icon-256x256.png" alt=""> Kyoo
|
||||
|
||||
Kyoo is an open-source media browser which allow you to stream your movies, tv-shows or anime.
|
||||
It is an alternative to Plex, Emby or Jellyfin.
|
||||
Welcome to Kyoo, the next-generation open-source media browser that redefines your streaming experience. Designed from the ground up, Kyoo stands out as a powerful alternative to Plex and Jellyfin. Unleash the full potential of your media library with cutting-edge features and a commitment to being free and open-source.
|
||||
|
||||
Kyoo has been created from scratch, it is not a fork. Everything is and always will be free and open-source.
|
||||

|
||||
|
||||
Feel free to open issues or pull requests, all contribution are welcomed.
|
||||
## 🌐 Getting Started
|
||||
|
||||
## Getting started
|
||||
- **[Installation](./INSTALLING.md):** Set up Kyoo effortlessly to enjoy seamless streaming of your favorite movies, TV shows, or anime.
|
||||
- **[Join the discord](https://discord.gg/E6Apw3aFaA):** Join our Discord community for discussions and support
|
||||
- **[API Documentation](https://kyoo.zoriya.dev/api/doc):** Dive into our comprehensive API documentation to explore advanced functionalities.
|
||||
- **[Contributing](./CONTRIBUTING.md):** Feel free to open issues, submit pull requests, and contribute to making Kyoo even better.
|
||||
|
||||
- [Installation](./INSTALLING.md)
|
||||
- [Api Documentation](https://kyoo.zoriya.dev/api/doc)
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
## 🚀 Features
|
||||
|
||||
## Features
|
||||
- Automatically organize your library of movies, tv or animes
|
||||
- No configuration or manual edit needed
|
||||
- guessit, thexem and themoviedb are used to retrive metadata even if your files are named in absurd ways
|
||||
- Transmux/Transcode files to make them available on every platform
|
||||
- Create smart watchlists that will automatically update when you watch movies/episodes
|
||||
- Download files for offline support (your watchlists will still be updated)
|
||||
- Android and web apps
|
||||
- Search your collection via names, tags or descriptions
|
||||
- Download metadata automatically
|
||||
- Handle subtitles natively with embedded fonts (ass, subrip or vtt)
|
||||
- Entirely free and works without internet (when metadata have already been downloaded)
|
||||
- **Dynamic Transcoding:** Transcode your media to any quality, change on the fly with auto quality, and seek effortlessly without waiting for the transcoder.
|
||||
|
||||
- **Auto Watch History:** Enjoy automatic watch history with continue watching, allowing you to quickly resume your series or discover new episodes.
|
||||
|
||||
## Live Demo
|
||||
- **Intelligent Metadata Retrieval:** Experience smart metadata retrieval, even for oddly named files, thanks to the power of guessit and themoviedb. It even uses thexem for enhanced anime handling.
|
||||
|
||||
You can see a live demo with copyright-free movies here: [kyoo.zoriya.dev](https://kyoo.zoriya.dev).
|
||||
Thanks to the [blender studio](https://www.blender.org/about/studio/) for providing open-source movies available for all.
|
||||
- **Cross-Platform Access:** Access Kyoo on Android and web clients, ensuring your media is at your fingertips wherever you go.
|
||||
|
||||
## Screens
|
||||
- **Meilisearch-Powered Search:** Utilize our advanced, typo-resilient search system powered by Meilisearch for lightning-fast results.
|
||||
|
||||

|
||||
- - -
|
||||

|
||||
- - -
|
||||

|
||||
- **Fast Scrubbing Support:** Navigate your media effortlessly with fast scrubbing support, enhancing your control over playback.
|
||||
|
||||
- **Download and Offline Support:** Enjoy the freedom to download and watch offline, with the watch history seamlessly updating when you reconnect.
|
||||
|
||||
- **Enhanced Subtitle Support:** Kyoo goes beyond the basics with enhanced subtitle support, including SSA/ASS formats and customizable fonts.
|
||||
|
||||
- **OIDC and Scrubbing Support:** Login with your favorites services (Google, Discord or any OIDC compliant service) and automatically mark episodes as watched on linked services (SIMKL and soon others).
|
||||
|
||||
## 🌟 Philosophy: Setup Once, Enjoy Forever
|
||||
|
||||
Kyoo's philosophy revolves around simplicity. Set it up once, forget about configuration hassles. Once installed, your library undergoes automatic scanning, adding new episodes or movies as soon as they're moved into your library's folder. No need for a specific file structure or meticulously renamed files – Kyoo does the right thing™.
|
||||
|
||||
## 📜 Why another media-browser?
|
||||
|
||||
From a technical standpoint, both Jellyfin and Plex lean on SQLite and confine everything within a single container, Kyoo takes a different route. We're not afraid to bring in additional containers when it makes sense – whether for specialized features like Meilisearch powering our search system or for scalability, as seen with our transcoder.
|
||||
|
||||
Kyoo embraces the "setup once, forget about it" philosophy. Unlike Plex and Jellyfin, we don't burden you with manual file renaming or specific folder structures. Kyoo seamlessly works with files straight from your download directory, minimizing the maintenance headache for server admins.
|
||||
|
||||
Kyoo narrows its focus to movies, TV shows, and anime streaming. No music, ebooks, or games – just pure cinematic delight.
|
||||
|
||||
## 🔗 Live Demo
|
||||
|
||||
Curious to see Kyoo in action? Check out our live demo featuring copyright-free movies at [kyoo.zoriya.dev](https://kyoo.zoriya.dev). Special thanks to the Blender Studio for providing open-source movies available for all.
|
||||
|
||||
## 👀 Screens
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/zoriya/kyoo/screens/android-movie.png" alt="Android Movie" width="350"></p>
|
||||
|
||||
Ready to elevate your streaming experience? Dive into Kyoo now! 🎬🎉
|
||||
|
||||
1
autosync/.gitignore
vendored
Normal file
1
autosync/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
8
autosync/Dockerfile
Normal file
8
autosync/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM python:3.12
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./requirements.txt .
|
||||
RUN pip3 install -r ./requirements.txt
|
||||
|
||||
COPY . .
|
||||
ENTRYPOINT ["python3", "-m", "autosync"]
|
||||
66
autosync/autosync/__init__.py
Normal file
66
autosync/autosync/__init__.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import dataclasses_json
|
||||
from datetime import datetime
|
||||
from marshmallow import fields
|
||||
|
||||
dataclasses_json.cfg.global_config.encoders[datetime] = datetime.isoformat
|
||||
dataclasses_json.cfg.global_config.decoders[datetime] = datetime.fromisoformat
|
||||
dataclasses_json.cfg.global_config.mm_fields[datetime] = fields.DateTime(format="iso")
|
||||
dataclasses_json.cfg.global_config.encoders[datetime | None] = datetime.isoformat
|
||||
dataclasses_json.cfg.global_config.decoders[datetime | None] = datetime.fromisoformat
|
||||
dataclasses_json.cfg.global_config.mm_fields[datetime | None] = fields.DateTime(
|
||||
format="iso"
|
||||
)
|
||||
|
||||
import pika
|
||||
from pika import spec
|
||||
from pika.adapters.blocking_connection import BlockingChannel
|
||||
import pika.credentials
|
||||
from autosync.models.message import Message
|
||||
from autosync.services.aggregate import Aggregate
|
||||
|
||||
from autosync.services.simkl import Simkl
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
service = Aggregate([Simkl()])
|
||||
|
||||
|
||||
def on_message(
|
||||
ch: BlockingChannel,
|
||||
method: spec.Basic.Deliver,
|
||||
properties: spec.BasicProperties,
|
||||
body: bytes,
|
||||
):
|
||||
try:
|
||||
message = Message.from_json(body) # type: Message
|
||||
service.update(message.value.user, message.value.resource, message.value)
|
||||
except Exception as e:
|
||||
logging.exception("Error processing message.", exc_info=e)
|
||||
logging.exception("Body: %s", body)
|
||||
|
||||
|
||||
def main():
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(
|
||||
host=os.environ.get("RABBITMQ_HOST", "rabbitmq"),
|
||||
credentials=pika.credentials.PlainCredentials(
|
||||
os.environ.get("RABBITMQ_DEFAULT_USER", "guest"),
|
||||
os.environ.get("RABBITMQ_DEFAULT_PASS", "guest"),
|
||||
),
|
||||
)
|
||||
)
|
||||
channel = connection.channel()
|
||||
|
||||
channel.exchange_declare(exchange="events.watched", exchange_type="topic")
|
||||
result = channel.queue_declare("", exclusive=True)
|
||||
queue_name = result.method.queue
|
||||
channel.queue_bind(exchange="events.watched", queue=queue_name, routing_key="#")
|
||||
|
||||
channel.basic_consume(
|
||||
queue=queue_name, on_message_callback=on_message, auto_ack=True
|
||||
)
|
||||
logging.info("Listening for autosync.")
|
||||
channel.start_consuming()
|
||||
4
autosync/autosync/__main__.py
Normal file
4
autosync/autosync/__main__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
import autosync
|
||||
|
||||
autosync.main()
|
||||
18
autosync/autosync/models/episode.py
Normal file
18
autosync/autosync/models/episode.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import Literal
|
||||
from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json, LetterCase
|
||||
|
||||
from autosync.models.show import Show
|
||||
|
||||
from .metadataid import MetadataID
|
||||
|
||||
|
||||
@dataclass_json(letter_case=LetterCase.CAMEL)
|
||||
@dataclass
|
||||
class Episode:
|
||||
external_id: dict[str, MetadataID]
|
||||
show: Show
|
||||
season_number: int
|
||||
episode_number: int
|
||||
absolute_number: int
|
||||
kind: Literal["episode"]
|
||||
23
autosync/autosync/models/message.py
Normal file
23
autosync/autosync/models/message.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json, LetterCase
|
||||
|
||||
from autosync.models.episode import Episode
|
||||
from autosync.models.movie import Movie
|
||||
from autosync.models.show import Show
|
||||
from autosync.models.user import User
|
||||
from autosync.models.watch_status import WatchStatus
|
||||
|
||||
|
||||
@dataclass_json(letter_case=LetterCase.CAMEL)
|
||||
@dataclass
|
||||
class WatchStatusMessage(WatchStatus):
|
||||
user: User
|
||||
resource: Movie | Show | Episode
|
||||
|
||||
|
||||
@dataclass_json(letter_case=LetterCase.CAMEL)
|
||||
@dataclass
|
||||
class Message:
|
||||
action: str
|
||||
type: str
|
||||
value: WatchStatusMessage
|
||||
10
autosync/autosync/models/metadataid.py
Normal file
10
autosync/autosync/models/metadataid.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json, LetterCase
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass_json(letter_case=LetterCase.CAMEL)
|
||||
@dataclass
|
||||
class MetadataID:
|
||||
data_id: str
|
||||
link: Optional[str]
|
||||
19
autosync/autosync/models/movie.py
Normal file
19
autosync/autosync/models/movie.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from typing import Literal, Optional
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json, LetterCase
|
||||
|
||||
from .metadataid import MetadataID
|
||||
|
||||
|
||||
@dataclass_json(letter_case=LetterCase.CAMEL)
|
||||
@dataclass
|
||||
class Movie:
|
||||
name: str
|
||||
air_date: Optional[datetime]
|
||||
external_id: dict[str, MetadataID]
|
||||
kind: Literal["movie"]
|
||||
|
||||
@property
|
||||
def year(self):
|
||||
return self.air_date.year if self.air_date is not None else None
|
||||
19
autosync/autosync/models/show.py
Normal file
19
autosync/autosync/models/show.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from typing import Literal, Optional
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json, LetterCase
|
||||
|
||||
from .metadataid import MetadataID
|
||||
|
||||
|
||||
@dataclass_json(letter_case=LetterCase.CAMEL)
|
||||
@dataclass
|
||||
class Show:
|
||||
name: str
|
||||
start_air: Optional[datetime]
|
||||
external_id: dict[str, MetadataID]
|
||||
kind: Literal["show"]
|
||||
|
||||
@property
|
||||
def year(self):
|
||||
return self.start_air.year if self.start_air is not None else None
|
||||
34
autosync/autosync/models/user.py
Normal file
34
autosync/autosync/models/user.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from datetime import datetime, time
|
||||
from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json, LetterCase
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass_json(letter_case=LetterCase.CAMEL)
|
||||
@dataclass
|
||||
class JwtToken:
|
||||
token_type: str
|
||||
access_token: str
|
||||
refresh_token: Optional[str]
|
||||
expire_in: time
|
||||
expire_at: datetime
|
||||
|
||||
|
||||
@dataclass_json(letter_case=LetterCase.CAMEL)
|
||||
@dataclass
|
||||
class ExternalToken:
|
||||
id: str
|
||||
username: str
|
||||
profileUrl: Optional[str]
|
||||
token: JwtToken
|
||||
|
||||
|
||||
@dataclass_json(letter_case=LetterCase.CAMEL)
|
||||
@dataclass
|
||||
class User:
|
||||
id: str
|
||||
username: str
|
||||
email: str
|
||||
permissions: list[str]
|
||||
settings: dict[str, str]
|
||||
external_id: dict[str, ExternalToken]
|
||||
23
autosync/autosync/models/watch_status.py
Normal file
23
autosync/autosync/models/watch_status.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
from dataclasses_json import dataclass_json, LetterCase
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Status(str, Enum):
|
||||
COMPLETED = "Completed"
|
||||
WATCHING = "Watching"
|
||||
DROPED = "Droped"
|
||||
PLANNED = "Planned"
|
||||
DELETED = "Deleted"
|
||||
|
||||
|
||||
@dataclass_json(letter_case=LetterCase.CAMEL)
|
||||
@dataclass
|
||||
class WatchStatus:
|
||||
added_date: datetime
|
||||
played_date: Optional[datetime]
|
||||
status: Status
|
||||
watched_time: Optional[int]
|
||||
watched_percent: Optional[int]
|
||||
26
autosync/autosync/services/aggregate.py
Normal file
26
autosync/autosync/services/aggregate.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import logging
|
||||
from autosync.services.service import Service
|
||||
from ..models.user import User
|
||||
from ..models.show import Show
|
||||
from ..models.movie import Movie
|
||||
from ..models.episode import Episode
|
||||
from ..models.watch_status import WatchStatus
|
||||
|
||||
|
||||
class Aggregate(Service):
|
||||
def __init__(self, services: list[Service]):
|
||||
self._services = [x for x in services if x.enabled]
|
||||
logging.info("Autosync enabled with %s", [x.name for x in self._services])
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "aggragate"
|
||||
|
||||
def update(self, user: User, resource: Movie | Show | Episode, status: WatchStatus):
|
||||
for service in self._services:
|
||||
try:
|
||||
service.update(user, resource, status)
|
||||
except Exception as e:
|
||||
logging.exception(
|
||||
"Unhandled error on autosync %s:", service.name, exc_info=e
|
||||
)
|
||||
21
autosync/autosync/services/service.py
Normal file
21
autosync/autosync/services/service.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from abc import abstractmethod, abstractproperty
|
||||
|
||||
from ..models.user import User
|
||||
from ..models.show import Show
|
||||
from ..models.movie import Movie
|
||||
from ..models.episode import Episode
|
||||
from ..models.watch_status import WatchStatus
|
||||
|
||||
|
||||
class Service:
|
||||
@abstractproperty
|
||||
def name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractproperty
|
||||
def enabled(self) -> bool:
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def update(self, user: User, resource: Movie | Show | Episode, status: WatchStatus):
|
||||
raise NotImplementedError
|
||||
115
autosync/autosync/services/simkl.py
Normal file
115
autosync/autosync/services/simkl.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import os
|
||||
import requests
|
||||
import logging
|
||||
from autosync.models.metadataid import MetadataID
|
||||
|
||||
from autosync.services.service import Service
|
||||
from ..models.user import User
|
||||
from ..models.show import Show
|
||||
from ..models.movie import Movie
|
||||
from ..models.episode import Episode
|
||||
from ..models.watch_status import WatchStatus, Status
|
||||
|
||||
|
||||
class Simkl(Service):
|
||||
def __init__(self) -> None:
|
||||
self._api_key = os.environ.get("OIDC_SIMKL_CLIENTID")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "simkl"
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return self._api_key is not None
|
||||
|
||||
def update(self, user: User, resource: Movie | Show | Episode, status: WatchStatus):
|
||||
if "simkl" not in user.external_id or self._api_key is None:
|
||||
return
|
||||
|
||||
watch_date = status.played_date or status.added_date
|
||||
|
||||
if resource.kind == "episode":
|
||||
if status.status != Status.COMPLETED:
|
||||
return
|
||||
|
||||
resp = requests.post(
|
||||
"https://api.simkl.com/sync/history",
|
||||
json={
|
||||
"shows": [
|
||||
{
|
||||
"watched_at": watch_date.isoformat(),
|
||||
"title": resource.show.name,
|
||||
"year": resource.show.year,
|
||||
"ids": self._map_external_ids(resource.show.external_id),
|
||||
"seasons": [
|
||||
{
|
||||
"number": resource.season_number,
|
||||
"episodes": [{"number": resource.episode_number}],
|
||||
},
|
||||
{
|
||||
"number": 1,
|
||||
"episodes": [{"number": resource.absolute_number}],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Bearer {user.external_id["simkl"].token.access_token}",
|
||||
"simkl-api-key": self._api_key,
|
||||
},
|
||||
)
|
||||
logging.info("Simkl response: %s %s", resp.status_code, resp.text)
|
||||
return
|
||||
|
||||
category = "movies" if resource.kind == "movie" else "shows"
|
||||
|
||||
simkl_status = self._map_status(status.status)
|
||||
if simkl_status is None:
|
||||
return
|
||||
|
||||
resp = requests.post(
|
||||
"https://api.simkl.com/sync/add-to-list",
|
||||
json={
|
||||
category: [
|
||||
{
|
||||
"to": simkl_status,
|
||||
"watched_at": watch_date.isoformat()
|
||||
if status.status == Status.COMPLETED
|
||||
else None,
|
||||
"title": resource.name,
|
||||
"year": resource.year,
|
||||
"ids": self._map_external_ids(resource.external_id),
|
||||
}
|
||||
]
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Bearer {user.external_id["simkl"].token.access_token}",
|
||||
"simkl-api-key": self._api_key,
|
||||
},
|
||||
)
|
||||
logging.info("Simkl response: %s %s", resp.status_code, resp.text)
|
||||
|
||||
def _map_status(self, status: Status):
|
||||
match status:
|
||||
case Status.COMPLETED:
|
||||
return "completed"
|
||||
case Status.WATCHING:
|
||||
return "watching"
|
||||
case Status.COMPLETED:
|
||||
return "completed"
|
||||
case Status.PLANNED:
|
||||
return "plantowatch"
|
||||
case Status.DELETED:
|
||||
# do not delete items on simkl, most of deleted status are for a rewatch.
|
||||
return None
|
||||
case _:
|
||||
return None
|
||||
|
||||
def _map_external_ids(self, ids: dict[str, MetadataID]):
|
||||
return {service: id.data_id for service, id in ids.items()} | {
|
||||
"tmdb": int(ids["themoviedatabase"].data_id)
|
||||
if "themoviedatabase" in ids
|
||||
else None
|
||||
}
|
||||
2
autosync/pyproject.toml
Normal file
2
autosync/pyproject.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[tool.ruff.format]
|
||||
indent-style = "tab"
|
||||
3
autosync/requirements.txt
Normal file
3
autosync/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
pika
|
||||
requests
|
||||
dataclasses-json
|
||||
@@ -3,13 +3,13 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "7.0.9",
|
||||
"version": "8.0.3",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
},
|
||||
"csharpier": {
|
||||
"version": "0.26.4",
|
||||
"version": "0.27.2",
|
||||
"commands": [
|
||||
"dotnet-csharpier"
|
||||
]
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
Dockerfile
|
||||
Dockerfile.dev
|
||||
Dockerfile.*
|
||||
.dockerignore
|
||||
.gitignore
|
||||
docker-compose.yml
|
||||
README.md
|
||||
**/build
|
||||
**/dist
|
||||
src/Kyoo.WebApp/Front/nodes_modules
|
||||
**/bin
|
||||
**/obj
|
||||
out
|
||||
docs
|
||||
tests
|
||||
!tests/Kyoo.Tests/Kyoo.Tests.csproj
|
||||
front
|
||||
video
|
||||
nginx.conf.template
|
||||
|
||||
@@ -16,6 +16,8 @@ dotnet_diagnostic.IDE0055.severity = none
|
||||
dotnet_diagnostic.IDE0058.severity = none
|
||||
dotnet_diagnostic.IDE0130.severity = none
|
||||
|
||||
# Convert to file-scoped namespace
|
||||
csharp_style_namespace_declarations = file_scoped:warning
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
csharp_using_directive_placement = outside_namespace:warning
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:7.0 as builder
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 as builder
|
||||
ARG TARGETARCH
|
||||
WORKDIR /kyoo
|
||||
|
||||
@@ -11,15 +11,15 @@ COPY src/Kyoo.Core/Kyoo.Core.csproj src/Kyoo.Core/Kyoo.Core.csproj
|
||||
COPY src/Kyoo.Host/Kyoo.Host.csproj src/Kyoo.Host/Kyoo.Host.csproj
|
||||
COPY src/Kyoo.Postgresql/Kyoo.Postgresql.csproj src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
|
||||
COPY src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj
|
||||
COPY src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj
|
||||
COPY src/Kyoo.Swagger/Kyoo.Swagger.csproj src/Kyoo.Swagger/Kyoo.Swagger.csproj
|
||||
COPY tests/Kyoo.Tests/Kyoo.Tests.csproj tests/Kyoo.Tests/Kyoo.Tests.csproj
|
||||
RUN dotnet restore -a $TARGETARCH
|
||||
|
||||
COPY . .
|
||||
ARG VERSION
|
||||
RUN dotnet publish -a $TARGETARCH --no-restore -c Release -o /app "-p:Version=${VERSION:-"0.0.0-dev"}" src/Kyoo.Host
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:7.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
RUN apt-get update && apt-get install -y curl
|
||||
COPY --from=builder /app /app
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0
|
||||
RUN apt-get update && apt-get install -y curl
|
||||
WORKDIR /app
|
||||
|
||||
@@ -11,8 +11,8 @@ COPY src/Kyoo.Core/Kyoo.Core.csproj src/Kyoo.Core/Kyoo.Core.csproj
|
||||
COPY src/Kyoo.Host/Kyoo.Host.csproj src/Kyoo.Host/Kyoo.Host.csproj
|
||||
COPY src/Kyoo.Postgresql/Kyoo.Postgresql.csproj src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
|
||||
COPY src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj
|
||||
COPY src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj
|
||||
COPY src/Kyoo.Swagger/Kyoo.Swagger.csproj src/Kyoo.Swagger/Kyoo.Swagger.csproj
|
||||
COPY tests/Kyoo.Tests/Kyoo.Tests.csproj tests/Kyoo.Tests/Kyoo.Tests.csproj
|
||||
RUN dotnet restore
|
||||
|
||||
WORKDIR /kyoo
|
||||
@@ -20,4 +20,4 @@ EXPOSE 5000
|
||||
ENV DOTNET_USE_POLLING_FILE_WATCHER 1
|
||||
# HEALTHCHECK --interval=5s CMD curl --fail http://localhost:5000/health || exit
|
||||
HEALTHCHECK CMD true
|
||||
ENTRYPOINT ["dotnet", "watch", "run", "--no-restore", "--project", "/app/src/Kyoo.Host"]
|
||||
ENTRYPOINT ["dotnet", "watch", "--non-interactive", "run", "--no-restore", "--project", "/app/src/Kyoo.Host"]
|
||||
|
||||
28
back/Dockerfile.migrations
Normal file
28
back/Dockerfile.migrations
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 as builder
|
||||
ARG TARGETARCH
|
||||
WORKDIR /kyoo
|
||||
|
||||
COPY .config/dotnet-tools.json .config/dotnet-tools.json
|
||||
RUN dotnet tool restore
|
||||
|
||||
COPY Kyoo.sln ./Kyoo.sln
|
||||
COPY nuget.config ./nuget.config
|
||||
COPY src/Directory.Build.props src/Directory.Build.props
|
||||
COPY src/Kyoo.Authentication/Kyoo.Authentication.csproj src/Kyoo.Authentication/Kyoo.Authentication.csproj
|
||||
COPY src/Kyoo.Abstractions/Kyoo.Abstractions.csproj src/Kyoo.Abstractions/Kyoo.Abstractions.csproj
|
||||
COPY src/Kyoo.Core/Kyoo.Core.csproj src/Kyoo.Core/Kyoo.Core.csproj
|
||||
COPY src/Kyoo.Host/Kyoo.Host.csproj src/Kyoo.Host/Kyoo.Host.csproj
|
||||
COPY src/Kyoo.Postgresql/Kyoo.Postgresql.csproj src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
|
||||
COPY src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj
|
||||
COPY src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj
|
||||
COPY src/Kyoo.Swagger/Kyoo.Swagger.csproj src/Kyoo.Swagger/Kyoo.Swagger.csproj
|
||||
RUN dotnet restore -a $TARGETARCH
|
||||
|
||||
COPY . .
|
||||
RUN dotnet build
|
||||
RUN dotnet ef migrations bundle --no-build --self-contained -r linux-${TARGETARCH} -f -o /app/migrate -p src/Kyoo.Postgresql --verbose
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0
|
||||
COPY --from=builder /app/migrate /app/migrate
|
||||
|
||||
ENTRYPOINT /app/migrate --connection "USER ID=${POSTGRES_USER};PASSWORD=${POSTGRES_PASSWORD};SERVER=postgres;PORT=5432;DATABASE=${POSTGRES_DB};"
|
||||
@@ -1,4 +1,5 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
#
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kyoo.Core", "src\Kyoo.Core\Kyoo.Core.csproj", "{0F8275B6-C7DD-42DF-A168-755C81B1C329}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Abstractions", "src\Kyoo.Abstractions\Kyoo.Abstractions.csproj", "{BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}"
|
||||
@@ -7,16 +8,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "src\Kyoo
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "src\Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Tests", "tests\Kyoo.Tests\Kyoo.Tests.csproj", "{0C8AA7EA-E723-4532-852F-35AA4E8AFED5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Swagger", "src\Kyoo.Swagger\Kyoo.Swagger.csproj", "{7D1A7596-73F6-4D35-842E-A5AD9C620596}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{FEAE1B0E-D797-470F-9030-0EF743575ECC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Host", "src\Kyoo.Host\Kyoo.Host.csproj", "{0938459E-2E2B-457F-8120-7D8CA93866A6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Meilisearch", "src\Kyoo.Meilisearch\Kyoo.Meilisearch.csproj", "{F8E6018A-FD51-40EB-99FF-A26BA59F2762}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.RabbitMq", "src\Kyoo.RabbitMq\Kyoo.RabbitMq.csproj", "{B97AD4A8-E6E6-41CD-87DF-5F1326FD7198}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -43,10 +42,6 @@ Global
|
||||
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0C8AA7EA-E723-4532-852F-35AA4E8AFED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0C8AA7EA-E723-4532-852F-35AA4E8AFED5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0C8AA7EA-E723-4532-852F-35AA4E8AFED5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0C8AA7EA-E723-4532-852F-35AA4E8AFED5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -67,8 +62,9 @@ Global
|
||||
{F8E6018A-FD51-40EB-99FF-A26BA59F2762}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F8E6018A-FD51-40EB-99FF-A26BA59F2762}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F8E6018A-FD51-40EB-99FF-A26BA59F2762}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{0C8AA7EA-E723-4532-852F-35AA4E8AFED5} = {FEAE1B0E-D797-470F-9030-0EF743575ECC}
|
||||
{B97AD4A8-E6E6-41CD-87DF-5F1326FD7198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B97AD4A8-E6E6-41CD-87DF-5F1326FD7198}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B97AD4A8-E6E6-41CD-87DF-5F1326FD7198}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B97AD4A8-E6E6-41CD-87DF-5F1326FD7198}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Company>Kyoo</Company>
|
||||
<Authors>Kyoo</Authors>
|
||||
@@ -23,11 +23,6 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<BaseIntermediateOutputPath>$(MsBuildThisFileDirectory)/../out/obj/$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<BaseOutputPath>$(MsBuildThisFileDirectory)/../out/bin/$(MSBuildProjectName)</BaseOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<CheckCodingStyle Condition="$(CheckCodingStyle) == ''">true</CheckCodingStyle>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -16,19 +16,20 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Xunit;
|
||||
using Kyoo.Abstractions.Models.Utils;
|
||||
|
||||
namespace Kyoo.Tests.Database
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
public interface IIssueRepository
|
||||
{
|
||||
public class GlobalTests
|
||||
{
|
||||
[Fact]
|
||||
[SuppressMessage("ReSharper", "EqualExpressionComparison")]
|
||||
public void SampleTest()
|
||||
{
|
||||
Assert.False(ReferenceEquals(TestSample.Get<Show>(), TestSample.Get<Show>()));
|
||||
}
|
||||
}
|
||||
Task<ICollection<Issue>> GetAll(Filter<Issue>? filter = default);
|
||||
|
||||
Task<int> GetCount(Filter<Issue>? filter = default);
|
||||
|
||||
Task<Issue> Upsert(Issue issue);
|
||||
|
||||
Task DeleteAll(Filter<Issue>? filter = default);
|
||||
}
|
||||
@@ -18,69 +18,63 @@
|
||||
|
||||
using Kyoo.Abstractions.Models;
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// An interface to interact with the database. Every repository is mapped through here.
|
||||
/// </summary>
|
||||
public interface ILibraryManager
|
||||
{
|
||||
IRepository<T> Repository<T>()
|
||||
where T : IResource, IQuery;
|
||||
|
||||
/// <summary>
|
||||
/// An interface to interact with the database. Every repository is mapped through here.
|
||||
/// The repository that handle libraries items (a wrapper around shows and collections).
|
||||
/// </summary>
|
||||
public interface ILibraryManager
|
||||
{
|
||||
IRepository<T> Repository<T>()
|
||||
where T : IResource, IQuery;
|
||||
IRepository<ILibraryItem> LibraryItems { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle libraries items (a wrapper around shows and collections).
|
||||
/// </summary>
|
||||
IRepository<ILibraryItem> LibraryItems { get; }
|
||||
/// <summary>
|
||||
/// The repository that handle new items.
|
||||
/// </summary>
|
||||
IRepository<INews> News { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle new items.
|
||||
/// </summary>
|
||||
IRepository<INews> News { get; }
|
||||
/// <summary>
|
||||
/// The repository that handle watched items.
|
||||
/// </summary>
|
||||
IWatchStatusRepository WatchStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle watched items.
|
||||
/// </summary>
|
||||
IWatchStatusRepository WatchStatus { get; }
|
||||
/// <summary>
|
||||
/// The repository that handle collections.
|
||||
/// </summary>
|
||||
IRepository<Collection> Collections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle collections.
|
||||
/// </summary>
|
||||
IRepository<Collection> Collections { get; }
|
||||
/// <summary>
|
||||
/// The repository that handle shows.
|
||||
/// </summary>
|
||||
IRepository<Movie> Movies { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle shows.
|
||||
/// </summary>
|
||||
IRepository<Movie> Movies { get; }
|
||||
/// <summary>
|
||||
/// The repository that handle shows.
|
||||
/// </summary>
|
||||
IRepository<Show> Shows { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle shows.
|
||||
/// </summary>
|
||||
IRepository<Show> Shows { get; }
|
||||
/// <summary>
|
||||
/// The repository that handle seasons.
|
||||
/// </summary>
|
||||
IRepository<Season> Seasons { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle seasons.
|
||||
/// </summary>
|
||||
IRepository<Season> Seasons { get; }
|
||||
/// <summary>
|
||||
/// The repository that handle episodes.
|
||||
/// </summary>
|
||||
IRepository<Episode> Episodes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle episodes.
|
||||
/// </summary>
|
||||
IRepository<Episode> Episodes { get; }
|
||||
/// <summary>
|
||||
/// The repository that handle studios.
|
||||
/// </summary>
|
||||
IRepository<Studio> Studios { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle people.
|
||||
/// </summary>
|
||||
IRepository<People> People { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle studios.
|
||||
/// </summary>
|
||||
IRepository<Studio> Studios { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The repository that handle users.
|
||||
/// </summary>
|
||||
IRepository<User> Users { get; }
|
||||
}
|
||||
/// <summary>
|
||||
/// The repository that handle users.
|
||||
/// </summary>
|
||||
IRepository<User> Users { get; }
|
||||
}
|
||||
|
||||
@@ -19,29 +19,28 @@
|
||||
using Kyoo.Abstractions.Models.Permissions;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// A service to validate permissions.
|
||||
/// </summary>
|
||||
public interface IPermissionValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// A service to validate permissions.
|
||||
/// Create an IAuthorizationFilter that will be used to validate permissions.
|
||||
/// This can registered with any lifetime.
|
||||
/// </summary>
|
||||
public interface IPermissionValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Create an IAuthorizationFilter that will be used to validate permissions.
|
||||
/// This can registered with any lifetime.
|
||||
/// </summary>
|
||||
/// <param name="attribute">The permission attribute to validate.</param>
|
||||
/// <returns>An authorization filter used to validate the permission.</returns>
|
||||
IFilterMetadata Create(PermissionAttribute attribute);
|
||||
/// <param name="attribute">The permission attribute to validate.</param>
|
||||
/// <returns>An authorization filter used to validate the permission.</returns>
|
||||
IFilterMetadata Create(PermissionAttribute attribute);
|
||||
|
||||
/// <summary>
|
||||
/// Create an IAuthorizationFilter that will be used to validate permissions.
|
||||
/// This can registered with any lifetime.
|
||||
/// </summary>
|
||||
/// <param name="attribute">
|
||||
/// A partial attribute to validate. See <see cref="PartialPermissionAttribute"/>.
|
||||
/// </param>
|
||||
/// <returns>An authorization filter used to validate the permission.</returns>
|
||||
IFilterMetadata Create(PartialPermissionAttribute attribute);
|
||||
}
|
||||
/// <summary>
|
||||
/// Create an IAuthorizationFilter that will be used to validate permissions.
|
||||
/// This can registered with any lifetime.
|
||||
/// </summary>
|
||||
/// <param name="attribute">
|
||||
/// A partial attribute to validate. See <see cref="PartialPermissionAttribute"/>.
|
||||
/// </param>
|
||||
/// <returns>An authorization filter used to validate the permission.</returns>
|
||||
IFilterMetadata Create(PartialPermissionAttribute attribute);
|
||||
}
|
||||
|
||||
@@ -19,50 +19,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Autofac;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// A common interface used to discord plugins
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You can inject services in the IPlugin constructor.
|
||||
/// You should only inject well known services like an ILogger, IConfiguration or IWebHostEnvironment.
|
||||
/// </remarks>
|
||||
public interface IPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// A common interface used to discord plugins
|
||||
/// The name of the plugin
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You can inject services in the IPlugin constructor.
|
||||
/// You should only inject well known services like an ILogger, IConfiguration or IWebHostEnvironment.
|
||||
/// </remarks>
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
|
||||
public interface IPlugin
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional configuration step to allow a plugin to change asp net configurations.
|
||||
/// </summary>
|
||||
/// <seealso cref="SA"/>
|
||||
IEnumerable<IStartupAction> ConfigureSteps => ArraySegment<IStartupAction>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// A configure method that will be run on plugin's startup.
|
||||
/// </summary>
|
||||
/// <param name="builder">The autofac service container to register services.</param>
|
||||
void Configure(ContainerBuilder builder)
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the plugin
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
// Skipped
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An optional configuration step to allow a plugin to change asp net configurations.
|
||||
/// </summary>
|
||||
/// <seealso cref="SA"/>
|
||||
IEnumerable<IStartupAction> ConfigureSteps => ArraySegment<IStartupAction>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// A configure method that will be run on plugin's startup.
|
||||
/// </summary>
|
||||
/// <param name="builder">The autofac service container to register services.</param>
|
||||
void Configure(ContainerBuilder builder)
|
||||
{
|
||||
// Skipped
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A configure method that will be run on plugin's startup.
|
||||
/// This is available for libraries that build upon a <see cref="IServiceCollection"/>, for more precise
|
||||
/// configuration use <see cref="Configure(Autofac.ContainerBuilder)"/>.
|
||||
/// </summary>
|
||||
/// <param name="services">A service container to register new services.</param>
|
||||
void Configure(IServiceCollection services)
|
||||
{
|
||||
// Skipped
|
||||
}
|
||||
/// <summary>
|
||||
/// A configure method that will be run on plugin's startup.
|
||||
/// This is available for libraries that build upon a <see cref="IServiceCollection"/>, for more precise
|
||||
/// configuration use <see cref="Configure(Autofac.ContainerBuilder)"/>.
|
||||
/// </summary>
|
||||
/// <param name="services">A service container to register new services.</param>
|
||||
void Configure(IServiceCollection services)
|
||||
{
|
||||
// Skipped
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,51 +20,50 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Kyoo.Abstractions.Models.Exceptions;
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// A manager to load plugins and retrieve information from them.
|
||||
/// </summary>
|
||||
public interface IPluginManager
|
||||
{
|
||||
/// <summary>
|
||||
/// A manager to load plugins and retrieve information from them.
|
||||
/// Get a single plugin that match the type and name given.
|
||||
/// </summary>
|
||||
public interface IPluginManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a single plugin that match the type and name given.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the plugin</param>
|
||||
/// <typeparam name="T">The type of the plugin</typeparam>
|
||||
/// <exception cref="ItemNotFoundException">If no plugins match the query</exception>
|
||||
/// <returns>A plugin that match the queries</returns>
|
||||
public T GetPlugin<T>(string name);
|
||||
/// <param name="name">The name of the plugin</param>
|
||||
/// <typeparam name="T">The type of the plugin</typeparam>
|
||||
/// <exception cref="ItemNotFoundException">If no plugins match the query</exception>
|
||||
/// <returns>A plugin that match the queries</returns>
|
||||
public T GetPlugin<T>(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Get all plugins of the given type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of plugins to get</typeparam>
|
||||
/// <returns>A list of plugins matching the given type or an empty list of none match.</returns>
|
||||
public ICollection<T> GetPlugins<T>();
|
||||
/// <summary>
|
||||
/// Get all plugins of the given type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of plugins to get</typeparam>
|
||||
/// <returns>A list of plugins matching the given type or an empty list of none match.</returns>
|
||||
public ICollection<T> GetPlugins<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Get all plugins currently running on Kyoo. This also includes deleted plugins if the app as not been restarted.
|
||||
/// </summary>
|
||||
/// <returns>All plugins currently loaded.</returns>
|
||||
public ICollection<IPlugin> GetAllPlugins();
|
||||
/// <summary>
|
||||
/// Get all plugins currently running on Kyoo. This also includes deleted plugins if the app as not been restarted.
|
||||
/// </summary>
|
||||
/// <returns>All plugins currently loaded.</returns>
|
||||
public ICollection<IPlugin> GetAllPlugins();
|
||||
|
||||
/// <summary>
|
||||
/// Load plugins and their dependencies from the plugin directory.
|
||||
/// </summary>
|
||||
/// <param name="plugins">
|
||||
/// An initial plugin list to use.
|
||||
/// You should not try to put plugins from the plugins directory here as they will get automatically loaded.
|
||||
/// </param>
|
||||
public void LoadPlugins(ICollection<IPlugin> plugins);
|
||||
/// <summary>
|
||||
/// Load plugins and their dependencies from the plugin directory.
|
||||
/// </summary>
|
||||
/// <param name="plugins">
|
||||
/// An initial plugin list to use.
|
||||
/// You should not try to put plugins from the plugins directory here as they will get automatically loaded.
|
||||
/// </param>
|
||||
public void LoadPlugins(ICollection<IPlugin> plugins);
|
||||
|
||||
/// <summary>
|
||||
/// Load plugins and their dependencies from the plugin directory.
|
||||
/// </summary>
|
||||
/// <param name="plugins">
|
||||
/// An initial plugin list to use.
|
||||
/// You should not try to put plugins from the plugins directory here as they will get automatically loaded.
|
||||
/// </param>
|
||||
public void LoadPlugins(params Type[] plugins);
|
||||
}
|
||||
/// <summary>
|
||||
/// Load plugins and their dependencies from the plugin directory.
|
||||
/// </summary>
|
||||
/// <param name="plugins">
|
||||
/// An initial plugin list to use.
|
||||
/// You should not try to put plugins from the plugins directory here as they will get automatically loaded.
|
||||
/// </param>
|
||||
public void LoadPlugins(params Type[] plugins);
|
||||
}
|
||||
|
||||
@@ -23,242 +23,245 @@ using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Abstractions.Models.Exceptions;
|
||||
using Kyoo.Abstractions.Models.Utils;
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// A common repository for every resources.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The resource's type that this repository manage.</typeparam>
|
||||
public interface IRepository<T> : IBaseRepository
|
||||
where T : IResource, IQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// A common repository for every resources.
|
||||
/// The event handler type for all events of this repository.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The resource's type that this repository manage.</typeparam>
|
||||
public interface IRepository<T> : IBaseRepository
|
||||
where T : IResource, IQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// The event handler type for all events of this repository.
|
||||
/// </summary>
|
||||
/// <param name="resource">The resource created/modified/deleted</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public delegate Task ResourceEventHandler(T resource);
|
||||
|
||||
/// <summary>
|
||||
/// Get a resource from it's ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the resource</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T> Get(Guid id, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a resource from it's slug.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the resource</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T> Get(string slug, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the first resource that match the predicate.
|
||||
/// </summary>
|
||||
/// <param name="filter">A predicate to filter the resource.</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
|
||||
/// <param name="reverse">Reverse the sort.</param>
|
||||
/// <param name="afterId">Select the first element after this id if it was in a list.</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T> Get(
|
||||
Filter<T> filter,
|
||||
Include<T>? include = default,
|
||||
Sort<T>? sortBy = default,
|
||||
bool reverse = false,
|
||||
Guid? afterId = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Get a resource from it's ID or null if it is not found.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the resource</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T?> GetOrDefault(Guid id, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a resource from it's slug or null if it is not found.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the resource</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T?> GetOrDefault(string slug, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the first resource that match the predicate or null if it is not found.
|
||||
/// </summary>
|
||||
/// <param name="filter">A predicate to filter the resource.</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
|
||||
/// <param name="reverse">Reverse the sort.</param>
|
||||
/// <param name="afterId">Select the first element after this id if it was in a list.</param>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T?> GetOrDefault(
|
||||
Filter<T>? filter,
|
||||
Include<T>? include = default,
|
||||
Sort<T>? sortBy = default,
|
||||
bool reverse = false,
|
||||
Guid? afterId = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Search for resources with the database.
|
||||
/// </summary>
|
||||
/// <param name="query">The query string.</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <returns>A list of resources found</returns>
|
||||
Task<ICollection<T>> Search(string query, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get every resources that match all filters
|
||||
/// </summary>
|
||||
/// <param name="filter">A filter predicate</param>
|
||||
/// <param name="sort">Sort information about the query (sort by, sort order)</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
|
||||
/// <returns>A list of resources that match every filters</returns>
|
||||
Task<ICollection<T>> GetAll(
|
||||
Filter<T>? filter = null,
|
||||
Sort<T>? sort = default,
|
||||
Include<T>? include = default,
|
||||
Pagination? limit = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Get the number of resources that match the filter's predicate.
|
||||
/// </summary>
|
||||
/// <param name="filter">A filter predicate</param>
|
||||
/// <returns>How many resources matched that filter</returns>
|
||||
Task<int> GetCount(Filter<T>? filter = null);
|
||||
|
||||
/// <summary>
|
||||
/// Map a list of ids to a list of items (keep the order).
|
||||
/// </summary>
|
||||
/// <param name="ids">The list of items id.</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <returns>A list of resources mapped from ids.</returns>
|
||||
Task<ICollection<T>> FromIds(IList<Guid> ids, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new resource.
|
||||
/// </summary>
|
||||
/// <param name="obj">The item to register</param>
|
||||
/// <returns>The resource registers and completed by database's information (related items and so on)</returns>
|
||||
Task<T> Create(T obj);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to create</param>
|
||||
/// <returns>The newly created item or the existing value if it existed.</returns>
|
||||
Task<T> CreateIfNotExists(T obj);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a resource has been created.
|
||||
/// </summary>
|
||||
static event ResourceEventHandler OnCreated;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that should be called after a resource has been created.
|
||||
/// </summary>
|
||||
/// <param name="obj">The resource newly created.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected static Task OnResourceCreated(T obj) =>
|
||||
OnCreated?.Invoke(obj) ?? Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Edit a resource and replace every property
|
||||
/// </summary>
|
||||
/// <param name="edited">The resource to edit, it's ID can't change.</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
|
||||
Task<T> Edit(T edited);
|
||||
|
||||
/// <summary>
|
||||
/// Edit only specific properties of a resource
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the resource to edit</param>
|
||||
/// <param name="patch">
|
||||
/// A method that will be called when you need to update every properties that you want to
|
||||
/// persist.
|
||||
/// </param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
|
||||
Task<T> Patch(Guid id, Func<T, T> patch);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a resource has been edited.
|
||||
/// </summary>
|
||||
static event ResourceEventHandler OnEdited;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that should be called after a resource has been edited.
|
||||
/// </summary>
|
||||
/// <param name="obj">The resource newly edited.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected static Task OnResourceEdited(T obj) =>
|
||||
OnEdited?.Invoke(obj) ?? Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Delete a resource by it's ID
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the resource</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task Delete(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a resource by it's slug
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the resource</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task Delete(string slug);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a resource
|
||||
/// </summary>
|
||||
/// <param name="obj">The resource to delete</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task Delete(T obj);
|
||||
|
||||
/// <summary>
|
||||
/// Delete all resources that match the predicate.
|
||||
/// </summary>
|
||||
/// <param name="filter">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task DeleteAll(Filter<T> filter);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a resource has been edited.
|
||||
/// </summary>
|
||||
static event ResourceEventHandler OnDeleted;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that should be called after a resource has been deleted.
|
||||
/// </summary>
|
||||
/// <param name="obj">The resource newly deleted.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected static Task OnResourceDeleted(T obj) =>
|
||||
OnDeleted?.Invoke(obj) ?? Task.CompletedTask;
|
||||
}
|
||||
/// <param name="resource">The resource created/modified/deleted</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public delegate Task ResourceEventHandler(T resource);
|
||||
|
||||
/// <summary>
|
||||
/// A base class for repositories. Every service implementing this will be handled by the <see cref="ILibraryManager"/>.
|
||||
/// Get a resource from it's ID.
|
||||
/// </summary>
|
||||
public interface IBaseRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// The type for witch this repository is responsible or null if non applicable.
|
||||
/// </summary>
|
||||
Type RepositoryType { get; }
|
||||
}
|
||||
/// <param name="id">The id of the resource</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T> Get(Guid id, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a resource from it's slug.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the resource</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T> Get(string slug, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the first resource that match the predicate.
|
||||
/// </summary>
|
||||
/// <param name="filter">A predicate to filter the resource.</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
|
||||
/// <param name="reverse">Reverse the sort.</param>
|
||||
/// <param name="afterId">Select the first element after this id if it was in a list.</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T> Get(
|
||||
Filter<T> filter,
|
||||
Include<T>? include = default,
|
||||
Sort<T>? sortBy = default,
|
||||
bool reverse = false,
|
||||
Guid? afterId = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Get a resource from it's ID or null if it is not found.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the resource</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T?> GetOrDefault(Guid id, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a resource from it's slug or null if it is not found.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the resource</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T?> GetOrDefault(string slug, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the first resource that match the predicate or null if it is not found.
|
||||
/// </summary>
|
||||
/// <param name="filter">A predicate to filter the resource.</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
|
||||
/// <param name="reverse">Reverse the sort.</param>
|
||||
/// <param name="afterId">Select the first element after this id if it was in a list.</param>
|
||||
/// <returns>The resource found</returns>
|
||||
Task<T?> GetOrDefault(
|
||||
Filter<T>? filter,
|
||||
Include<T>? include = default,
|
||||
Sort<T>? sortBy = default,
|
||||
bool reverse = false,
|
||||
Guid? afterId = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Search for resources with the database.
|
||||
/// </summary>
|
||||
/// <param name="query">The query string.</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <returns>A list of resources found</returns>
|
||||
Task<ICollection<T>> Search(string query, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get every resources that match all filters
|
||||
/// </summary>
|
||||
/// <param name="filter">A filter predicate</param>
|
||||
/// <param name="sort">Sort information about the query (sort by, sort order)</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
|
||||
/// <returns>A list of resources that match every filters</returns>
|
||||
Task<ICollection<T>> GetAll(
|
||||
Filter<T>? filter = null,
|
||||
Sort<T>? sort = default,
|
||||
Include<T>? include = default,
|
||||
Pagination? limit = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Get the number of resources that match the filter's predicate.
|
||||
/// </summary>
|
||||
/// <param name="filter">A filter predicate</param>
|
||||
/// <returns>How many resources matched that filter</returns>
|
||||
Task<int> GetCount(Filter<T>? filter = null);
|
||||
|
||||
/// <summary>
|
||||
/// Map a list of ids to a list of items (keep the order).
|
||||
/// </summary>
|
||||
/// <param name="ids">The list of items id.</param>
|
||||
/// <param name="include">The related fields to include.</param>
|
||||
/// <returns>A list of resources mapped from ids.</returns>
|
||||
Task<ICollection<T>> FromIds(IList<Guid> ids, Include<T>? include = default);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new resource.
|
||||
/// </summary>
|
||||
/// <param name="obj">The item to register</param>
|
||||
/// <returns>The resource registers and completed by database's information (related items and so on)</returns>
|
||||
Task<T> Create(T obj);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to create</param>
|
||||
/// <returns>The newly created item or the existing value if it existed.</returns>
|
||||
Task<T> CreateIfNotExists(T obj);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a resource has been created.
|
||||
/// </summary>
|
||||
static event ResourceEventHandler OnCreated;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that should be called after a resource has been created.
|
||||
/// </summary>
|
||||
/// <param name="obj">The resource newly created.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected static Task OnResourceCreated(T obj) => OnCreated?.Invoke(obj) ?? Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Edit a resource and replace every property
|
||||
/// </summary>
|
||||
/// <param name="edited">The resource to edit, it's ID can't change.</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
|
||||
Task<T> Edit(T edited);
|
||||
|
||||
/// <summary>
|
||||
/// Edit only specific properties of a resource
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the resource to edit</param>
|
||||
/// <param name="patch">
|
||||
/// A method that will be called when you need to update every properties that you want to
|
||||
/// persist.
|
||||
/// </param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
|
||||
Task<T> Patch(Guid id, Func<T, T> patch);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a resource has been edited.
|
||||
/// </summary>
|
||||
static event ResourceEventHandler OnEdited;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that should be called after a resource has been edited.
|
||||
/// </summary>
|
||||
/// <param name="obj">The resource newly edited.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected static Task OnResourceEdited(T obj) => OnEdited?.Invoke(obj) ?? Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Delete a resource by it's ID
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the resource</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task Delete(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a resource by it's slug
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the resource</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task Delete(string slug);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a resource
|
||||
/// </summary>
|
||||
/// <param name="obj">The resource to delete</param>
|
||||
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task Delete(T obj);
|
||||
|
||||
/// <summary>
|
||||
/// Delete all resources that match the predicate.
|
||||
/// </summary>
|
||||
/// <param name="filter">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task DeleteAll(Filter<T> filter);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a resource has been edited.
|
||||
/// </summary>
|
||||
static event ResourceEventHandler OnDeleted;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that should be called after a resource has been deleted.
|
||||
/// </summary>
|
||||
/// <param name="obj">The resource newly deleted.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
protected static Task OnResourceDeleted(T obj) => OnDeleted?.Invoke(obj) ?? Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A base class for repositories. Every service implementing this will be handled by the <see cref="ILibraryManager"/>.
|
||||
/// </summary>
|
||||
public interface IBaseRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// The type for witch this repository is responsible or null if non applicable.
|
||||
/// </summary>
|
||||
Type RepositoryType { get; }
|
||||
}
|
||||
|
||||
public interface IUserRepository : IRepository<User>
|
||||
{
|
||||
Task<User?> GetByExternalId(string provider, string id);
|
||||
Task<User> AddExternalToken(Guid userId, string provider, ExternalToken token);
|
||||
Task<User> DeleteExternalToken(Guid userId, string provider);
|
||||
}
|
||||
|
||||
@@ -16,50 +16,63 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Models;
|
||||
|
||||
#nullable enable
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers
|
||||
/// <summary>
|
||||
/// Download images and retrieve the path of those images for a resource.
|
||||
/// </summary>
|
||||
public interface IThumbnailsManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Download images and retrieve the path of those images for a resource.
|
||||
/// Download images of a specified item.
|
||||
/// If no images is available to download, do nothing and silently return.
|
||||
/// </summary>
|
||||
public interface IThumbnailsManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Download images of a specified item.
|
||||
/// If no images is available to download, do nothing and silently return.
|
||||
/// </summary>
|
||||
/// <param name="item">
|
||||
/// The item to cache images.
|
||||
/// </param>
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task DownloadImages<T>(T item)
|
||||
where T : IThumbnails;
|
||||
/// <param name="item">
|
||||
/// The item to cache images.
|
||||
/// </param>
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task DownloadImages<T>(T item)
|
||||
where T : IThumbnails;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the local path of an image of the given item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to retrieve the poster from.</param>
|
||||
/// <param name="image">The ID of the image.</param>
|
||||
/// <param name="quality">The quality of the image</param>
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns>The path of the image for the given resource or null if it does not exists.</returns>
|
||||
string GetImagePath<T>(T item, string image, ImageQuality quality)
|
||||
where T : IThumbnails;
|
||||
/// <summary>
|
||||
/// Retrieve the local path of an image of the given item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to retrieve the poster from.</param>
|
||||
/// <param name="image">The ID of the image.</param>
|
||||
/// <param name="quality">The quality of the image</param>
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns>The path of the image for the given resource or null if it does not exists.</returns>
|
||||
string GetImagePath<T>(T item, string image, ImageQuality quality)
|
||||
where T : IThumbnails;
|
||||
|
||||
/// <summary>
|
||||
/// Delete images associated with the item.
|
||||
/// </summary>
|
||||
/// <param name="item">
|
||||
/// The item with cached images.
|
||||
/// </param>
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task DeleteImages<T>(T item)
|
||||
where T : IThumbnails;
|
||||
}
|
||||
/// <summary>
|
||||
/// Delete images associated with the item.
|
||||
/// </summary>
|
||||
/// <param name="item">
|
||||
/// The item with cached images.
|
||||
/// </param>
|
||||
/// <typeparam name="T">The type of the item</typeparam>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task DeleteImages<T>(T item)
|
||||
where T : IThumbnails;
|
||||
|
||||
/// <summary>
|
||||
/// Set the user's profile picture
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the user. </param>
|
||||
/// <returns>The byte stream of the image. Null if no image exist.</returns>
|
||||
Task<Stream> GetUserImage(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Set the user's profile picture
|
||||
/// </summary>
|
||||
/// <param name="userId">The id of the user. </param>
|
||||
/// <param name="image">The byte stream of the image. Null to delete the image.</param>
|
||||
Task SetUserImage(Guid userId, Stream? image);
|
||||
}
|
||||
|
||||
@@ -29,12 +29,7 @@ namespace Kyoo.Abstractions.Controllers;
|
||||
/// </summary>
|
||||
public interface IWatchStatusRepository
|
||||
{
|
||||
// /// <summary>
|
||||
// /// The event handler type for all events of this repository.
|
||||
// /// </summary>
|
||||
// /// <param name="resource">The resource created/modified/deleted</param>
|
||||
// /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
// public delegate Task ResourceEventHandler(T resource);
|
||||
public delegate Task ResourceEventHandler<T>(T resource);
|
||||
|
||||
Task<ICollection<IWatchlist>> GetAll(
|
||||
Filter<IWatchlist>? filter = default,
|
||||
@@ -48,15 +43,24 @@ public interface IWatchStatusRepository
|
||||
Guid movieId,
|
||||
Guid userId,
|
||||
WatchStatus status,
|
||||
int? watchedTime
|
||||
int? watchedTime,
|
||||
int? percent
|
||||
);
|
||||
|
||||
static event ResourceEventHandler<WatchStatus<Movie>> OnMovieStatusChangedHandler;
|
||||
protected static Task OnMovieStatusChanged(WatchStatus<Movie> obj) =>
|
||||
OnMovieStatusChangedHandler?.Invoke(obj) ?? Task.CompletedTask;
|
||||
|
||||
Task DeleteMovieStatus(Guid movieId, Guid userId);
|
||||
|
||||
Task<ShowWatchStatus?> GetShowStatus(Guid showId, Guid userId);
|
||||
|
||||
Task<ShowWatchStatus?> SetShowStatus(Guid showId, Guid userId, WatchStatus status);
|
||||
|
||||
static event ResourceEventHandler<WatchStatus<Show>> OnShowStatusChangedHandler;
|
||||
protected static Task OnShowStatusChanged(WatchStatus<Show> obj) =>
|
||||
OnShowStatusChangedHandler?.Invoke(obj) ?? Task.CompletedTask;
|
||||
|
||||
Task DeleteShowStatus(Guid showId, Guid userId);
|
||||
|
||||
Task<EpisodeWatchStatus?> GetEpisodeStatus(Guid episodeId, Guid userId);
|
||||
@@ -67,8 +71,13 @@ public interface IWatchStatusRepository
|
||||
Guid episodeId,
|
||||
Guid userId,
|
||||
WatchStatus status,
|
||||
int? watchedTime
|
||||
int? watchedTime,
|
||||
int? percent
|
||||
);
|
||||
|
||||
static event ResourceEventHandler<WatchStatus<Episode>> OnEpisodeStatusChangedHandler;
|
||||
protected static Task OnEpisodeStatusChanged(WatchStatus<Episode> obj) =>
|
||||
OnEpisodeStatusChangedHandler?.Invoke(obj) ?? Task.CompletedTask;
|
||||
|
||||
Task DeleteEpisodeStatus(Guid episodeId, Guid userId);
|
||||
}
|
||||
|
||||
@@ -19,256 +19,252 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// A list of constant priorities used for <see cref="IStartupAction"/>'s <see cref="IStartupAction.Priority"/>.
|
||||
/// It also contains helper methods for creating new <see cref="StartupAction"/>.
|
||||
/// </summary>
|
||||
public static class SA
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of constant priorities used for <see cref="IStartupAction"/>'s <see cref="IStartupAction.Priority"/>.
|
||||
/// It also contains helper methods for creating new <see cref="StartupAction"/>.
|
||||
/// The highest predefined priority existing for <see cref="StartupAction"/>.
|
||||
/// </summary>
|
||||
public static class SA
|
||||
public const int Before = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// Items defining routing (see IApplicationBuilder.UseRouting use this priority.
|
||||
/// </summary>
|
||||
public const int Routing = 4000;
|
||||
|
||||
/// <summary>
|
||||
/// Actions defining new static files router use this priority.
|
||||
/// </summary>
|
||||
public const int StaticFiles = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// Actions calling IApplicationBuilder.UseAuthentication use this priority.
|
||||
/// </summary>
|
||||
public const int Authentication = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// Actions calling IApplicationBuilder.UseAuthorization use this priority.
|
||||
/// </summary>
|
||||
public const int Authorization = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Action adding endpoint should use this priority (with a negative modificator if there is a catchall).
|
||||
/// </summary>
|
||||
public const int Endpoint = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The lowest predefined priority existing for <see cref="StartupAction"/>.
|
||||
/// It should run after all other actions.
|
||||
/// </summary>
|
||||
public const int After = -1000;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run</param>
|
||||
/// <param name="priority">The priority of the new action</param>
|
||||
/// <returns>A new <see cref="StartupAction"/></returns>
|
||||
public static StartupAction New(Action action, int priority) => new(action, priority);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run</param>
|
||||
/// <param name="priority">The priority of the new action</param>
|
||||
/// <typeparam name="T">A dependency that this action will use.</typeparam>
|
||||
/// <returns>A new <see cref="StartupAction"/></returns>
|
||||
public static StartupAction<T> New<T>(Action<T> action, int priority)
|
||||
where T : notnull => new(action, priority);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run</param>
|
||||
/// <param name="priority">The priority of the new action</param>
|
||||
/// <typeparam name="T">A dependency that this action will use.</typeparam>
|
||||
/// <typeparam name="T2">A second dependency that this action will use.</typeparam>
|
||||
/// <returns>A new <see cref="StartupAction"/></returns>
|
||||
public static StartupAction<T, T2> New<T, T2>(Action<T, T2> action, int priority)
|
||||
where T : notnull
|
||||
where T2 : notnull => new(action, priority);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run</param>
|
||||
/// <param name="priority">The priority of the new action</param>
|
||||
/// <typeparam name="T">A dependency that this action will use.</typeparam>
|
||||
/// <typeparam name="T2">A second dependency that this action will use.</typeparam>
|
||||
/// <typeparam name="T3">A third dependency that this action will use.</typeparam>
|
||||
/// <returns>A new <see cref="StartupAction"/></returns>
|
||||
public static StartupAction<T, T2, T3> New<T, T2, T3>(Action<T, T2, T3> action, int priority)
|
||||
where T : notnull
|
||||
where T2 : notnull
|
||||
where T3 : notnull => new(action, priority);
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IStartupAction"/> with no dependencies.
|
||||
/// </summary>
|
||||
public class StartupAction : IStartupAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The highest predefined priority existing for <see cref="StartupAction"/>.
|
||||
/// The action to execute at startup.
|
||||
/// </summary>
|
||||
public const int Before = 5000;
|
||||
private readonly Action _action;
|
||||
|
||||
/// <summary>
|
||||
/// Items defining routing (see IApplicationBuilder.UseRouting use this priority.
|
||||
/// </summary>
|
||||
public const int Routing = 4000;
|
||||
|
||||
/// <summary>
|
||||
/// Actions defining new static files router use this priority.
|
||||
/// </summary>
|
||||
public const int StaticFiles = 3000;
|
||||
|
||||
/// <summary>
|
||||
/// Actions calling IApplicationBuilder.UseAuthentication use this priority.
|
||||
/// </summary>
|
||||
public const int Authentication = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// Actions calling IApplicationBuilder.UseAuthorization use this priority.
|
||||
/// </summary>
|
||||
public const int Authorization = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Action adding endpoint should use this priority (with a negative modificator if there is a catchall).
|
||||
/// </summary>
|
||||
public const int Endpoint = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The lowest predefined priority existing for <see cref="StartupAction"/>.
|
||||
/// It should run after all other actions.
|
||||
/// </summary>
|
||||
public const int After = -1000;
|
||||
/// <inheritdoc />
|
||||
public int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run</param>
|
||||
/// <param name="priority">The priority of the new action</param>
|
||||
/// <returns>A new <see cref="StartupAction"/></returns>
|
||||
public static StartupAction New(Action action, int priority) => new(action, priority);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run</param>
|
||||
/// <param name="priority">The priority of the new action</param>
|
||||
/// <typeparam name="T">A dependency that this action will use.</typeparam>
|
||||
/// <returns>A new <see cref="StartupAction"/></returns>
|
||||
public static StartupAction<T> New<T>(Action<T> action, int priority)
|
||||
where T : notnull => new(action, priority);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run</param>
|
||||
/// <param name="priority">The priority of the new action</param>
|
||||
/// <typeparam name="T">A dependency that this action will use.</typeparam>
|
||||
/// <typeparam name="T2">A second dependency that this action will use.</typeparam>
|
||||
/// <returns>A new <see cref="StartupAction"/></returns>
|
||||
public static StartupAction<T, T2> New<T, T2>(Action<T, T2> action, int priority)
|
||||
where T : notnull
|
||||
where T2 : notnull => new(action, priority);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run</param>
|
||||
/// <param name="priority">The priority of the new action</param>
|
||||
/// <typeparam name="T">A dependency that this action will use.</typeparam>
|
||||
/// <typeparam name="T2">A second dependency that this action will use.</typeparam>
|
||||
/// <typeparam name="T3">A third dependency that this action will use.</typeparam>
|
||||
/// <returns>A new <see cref="StartupAction"/></returns>
|
||||
public static StartupAction<T, T2, T3> New<T, T2, T3>(
|
||||
Action<T, T2, T3> action,
|
||||
int priority
|
||||
)
|
||||
where T : notnull
|
||||
where T2 : notnull
|
||||
where T3 : notnull => new(action, priority);
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IStartupAction"/> with no dependencies.
|
||||
/// </summary>
|
||||
public class StartupAction : IStartupAction
|
||||
/// <param name="action">The action to execute on startup.</param>
|
||||
/// <param name="priority">The priority of this action (see <see cref="Priority"/>).</param>
|
||||
public StartupAction(Action action, int priority)
|
||||
{
|
||||
/// <summary>
|
||||
/// The action to execute at startup.
|
||||
/// </summary>
|
||||
private readonly Action _action;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute on startup.</param>
|
||||
/// <param name="priority">The priority of this action (see <see cref="Priority"/>).</param>
|
||||
public StartupAction(Action action, int priority)
|
||||
{
|
||||
_action = action;
|
||||
Priority = priority;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Run(IServiceProvider provider)
|
||||
{
|
||||
_action.Invoke();
|
||||
}
|
||||
_action = action;
|
||||
Priority = priority;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IStartupAction"/> with one dependencies.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dependency to use.</typeparam>
|
||||
public class StartupAction<T> : IStartupAction
|
||||
where T : notnull
|
||||
/// <inheritdoc />
|
||||
public void Run(IServiceProvider provider)
|
||||
{
|
||||
/// <summary>
|
||||
/// The action to execute at startup.
|
||||
/// </summary>
|
||||
private readonly Action<T> _action;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute on startup.</param>
|
||||
/// <param name="priority">The priority of this action (see <see cref="Priority"/>).</param>
|
||||
public StartupAction(Action<T> action, int priority)
|
||||
{
|
||||
_action = action;
|
||||
Priority = priority;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Run(IServiceProvider provider)
|
||||
{
|
||||
_action.Invoke(provider.GetRequiredService<T>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IStartupAction"/> with two dependencies.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dependency to use.</typeparam>
|
||||
/// <typeparam name="T2">The second dependency to use.</typeparam>
|
||||
public class StartupAction<T, T2> : IStartupAction
|
||||
where T : notnull
|
||||
where T2 : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// The action to execute at startup.
|
||||
/// </summary>
|
||||
private readonly Action<T, T2> _action;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction{T, T2}"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute on startup.</param>
|
||||
/// <param name="priority">The priority of this action (see <see cref="Priority"/>).</param>
|
||||
public StartupAction(Action<T, T2> action, int priority)
|
||||
{
|
||||
_action = action;
|
||||
Priority = priority;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Run(IServiceProvider provider)
|
||||
{
|
||||
_action.Invoke(provider.GetRequiredService<T>(), provider.GetRequiredService<T2>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IStartupAction"/> with three dependencies.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dependency to use.</typeparam>
|
||||
/// <typeparam name="T2">The second dependency to use.</typeparam>
|
||||
/// <typeparam name="T3">The third dependency to use.</typeparam>
|
||||
public class StartupAction<T, T2, T3> : IStartupAction
|
||||
where T : notnull
|
||||
where T2 : notnull
|
||||
where T3 : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// The action to execute at startup.
|
||||
/// </summary>
|
||||
private readonly Action<T, T2, T3> _action;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction{T, T2, T3}"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute on startup.</param>
|
||||
/// <param name="priority">The priority of this action (see <see cref="Priority"/>).</param>
|
||||
public StartupAction(Action<T, T2, T3> action, int priority)
|
||||
{
|
||||
_action = action;
|
||||
Priority = priority;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Run(IServiceProvider provider)
|
||||
{
|
||||
_action.Invoke(
|
||||
provider.GetRequiredService<T>(),
|
||||
provider.GetRequiredService<T2>(),
|
||||
provider.GetRequiredService<T3>()
|
||||
);
|
||||
}
|
||||
_action.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An action executed on kyoo's startup to initialize the asp-net container.
|
||||
/// A <see cref="IStartupAction"/> with one dependencies.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the base interface, see <see cref="SA.StartupAction"/> for a simpler use of this.
|
||||
/// </remarks>
|
||||
public interface IStartupAction
|
||||
/// <typeparam name="T">The dependency to use.</typeparam>
|
||||
public class StartupAction<T> : IStartupAction
|
||||
where T : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// The priority of this action. The actions will be executed on descending priority order.
|
||||
/// If two actions have the same priority, their order is undefined.
|
||||
/// The action to execute at startup.
|
||||
/// </summary>
|
||||
int Priority { get; }
|
||||
private readonly Action<T> _action;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Run this action to configure the container, a service provider containing all services can be used.
|
||||
/// Create a new <see cref="StartupAction{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="provider">The service provider containing all services can be used.</param>
|
||||
void Run(IServiceProvider provider);
|
||||
/// <param name="action">The action to execute on startup.</param>
|
||||
/// <param name="priority">The priority of this action (see <see cref="Priority"/>).</param>
|
||||
public StartupAction(Action<T> action, int priority)
|
||||
{
|
||||
_action = action;
|
||||
Priority = priority;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Run(IServiceProvider provider)
|
||||
{
|
||||
_action.Invoke(provider.GetRequiredService<T>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IStartupAction"/> with two dependencies.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dependency to use.</typeparam>
|
||||
/// <typeparam name="T2">The second dependency to use.</typeparam>
|
||||
public class StartupAction<T, T2> : IStartupAction
|
||||
where T : notnull
|
||||
where T2 : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// The action to execute at startup.
|
||||
/// </summary>
|
||||
private readonly Action<T, T2> _action;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction{T, T2}"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute on startup.</param>
|
||||
/// <param name="priority">The priority of this action (see <see cref="Priority"/>).</param>
|
||||
public StartupAction(Action<T, T2> action, int priority)
|
||||
{
|
||||
_action = action;
|
||||
Priority = priority;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Run(IServiceProvider provider)
|
||||
{
|
||||
_action.Invoke(provider.GetRequiredService<T>(), provider.GetRequiredService<T2>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IStartupAction"/> with three dependencies.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The dependency to use.</typeparam>
|
||||
/// <typeparam name="T2">The second dependency to use.</typeparam>
|
||||
/// <typeparam name="T3">The third dependency to use.</typeparam>
|
||||
public class StartupAction<T, T2, T3> : IStartupAction
|
||||
where T : notnull
|
||||
where T2 : notnull
|
||||
where T3 : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// The action to execute at startup.
|
||||
/// </summary>
|
||||
private readonly Action<T, T2, T3> _action;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="StartupAction{T, T2, T3}"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute on startup.</param>
|
||||
/// <param name="priority">The priority of this action (see <see cref="Priority"/>).</param>
|
||||
public StartupAction(Action<T, T2, T3> action, int priority)
|
||||
{
|
||||
_action = action;
|
||||
Priority = priority;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Run(IServiceProvider provider)
|
||||
{
|
||||
_action.Invoke(
|
||||
provider.GetRequiredService<T>(),
|
||||
provider.GetRequiredService<T2>(),
|
||||
provider.GetRequiredService<T3>()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An action executed on kyoo's startup to initialize the asp-net container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the base interface, see <see cref="SA.StartupAction"/> for a simpler use of this.
|
||||
/// </remarks>
|
||||
public interface IStartupAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The priority of this action. The actions will be executed on descending priority order.
|
||||
/// If two actions have the same priority, their order is undefined.
|
||||
/// </summary>
|
||||
int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Run this action to configure the container, a service provider containing all services can be used.
|
||||
/// </summary>
|
||||
/// <param name="provider">The service provider containing all services can be used.</param>
|
||||
void Run(IServiceProvider provider);
|
||||
}
|
||||
|
||||
@@ -23,43 +23,42 @@ using System.Security.Claims;
|
||||
using Kyoo.Abstractions.Models.Exceptions;
|
||||
using Kyoo.Authentication.Models;
|
||||
|
||||
namespace Kyoo.Authentication
|
||||
namespace Kyoo.Authentication;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods.
|
||||
/// </summary>
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods.
|
||||
/// Get the permissions of an user.
|
||||
/// </summary>
|
||||
public static class Extensions
|
||||
/// <param name="user">The user</param>
|
||||
/// <returns>The list of permissions</returns>
|
||||
public static ICollection<string> GetPermissions(this ClaimsPrincipal user)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the permissions of an user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user</param>
|
||||
/// <returns>The list of permissions</returns>
|
||||
public static ICollection<string> GetPermissions(this ClaimsPrincipal user)
|
||||
{
|
||||
return user.Claims.FirstOrDefault(x => x.Type == Claims.Permissions)?.Value.Split(',')
|
||||
?? Array.Empty<string>();
|
||||
}
|
||||
return user.Claims.FirstOrDefault(x => x.Type == Claims.Permissions)?.Value.Split(',')
|
||||
?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the id of the current user or null if unlogged or invalid.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>The id of the user or null.</returns>
|
||||
public static Guid? GetId(this ClaimsPrincipal user)
|
||||
{
|
||||
Claim? value = user.FindFirst(Claims.Id);
|
||||
if (Guid.TryParse(value?.Value, out Guid id))
|
||||
return id;
|
||||
return null;
|
||||
}
|
||||
/// <summary>
|
||||
/// Get the id of the current user or null if unlogged or invalid.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>The id of the user or null.</returns>
|
||||
public static Guid? GetId(this ClaimsPrincipal user)
|
||||
{
|
||||
Claim? value = user.FindFirst(Claims.Id);
|
||||
if (Guid.TryParse(value?.Value, out Guid id))
|
||||
return id;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Guid GetIdOrThrow(this ClaimsPrincipal user)
|
||||
{
|
||||
Guid? ret = user.GetId();
|
||||
if (ret == null)
|
||||
throw new UnauthorizedException();
|
||||
return ret.Value;
|
||||
}
|
||||
public static Guid GetIdOrThrow(this ClaimsPrincipal user)
|
||||
{
|
||||
Guid? ret = user.GetId();
|
||||
if (ret == null)
|
||||
throw new UnauthorizedException();
|
||||
return ret.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="7.1.0" />
|
||||
<PackageReference Include="Dapper" Version="2.1.24" />
|
||||
<PackageReference Include="Autofac" Version="8.0.0" />
|
||||
<PackageReference Include="Dapper" Version="2.1.37" />
|
||||
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.1.4-prebeta" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2023.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Sprache" Version="2.3.1" />
|
||||
<PackageReference Include="System.ComponentModel.Composition" Version="7.0.0" />
|
||||
<PackageReference Include="System.ComponentModel.Composition" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -18,35 +18,34 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Attributes
|
||||
namespace Kyoo.Abstractions.Models.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// An attribute to specify on apis to specify it's documentation's name and category.
|
||||
/// If this is applied on a method, the specified method will be exploded from the controller's page and be
|
||||
/// included on the specified tag page.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class ApiDefinitionAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// An attribute to specify on apis to specify it's documentation's name and category.
|
||||
/// If this is applied on a method, the specified method will be exploded from the controller's page and be
|
||||
/// included on the specified tag page.
|
||||
/// The public name of this api.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class ApiDefinitionAttribute : Attribute
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the group in witch this API is. You can also specify a custom sort order using the following
|
||||
/// format: <code>order:name</code>. Everything before the first <c>:</c> will be removed but kept for
|
||||
/// th alphabetical ordering.
|
||||
/// </summary>
|
||||
public string? Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ApiDefinitionAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the api that will be used on the documentation page.</param>
|
||||
public ApiDefinitionAttribute(string name)
|
||||
{
|
||||
/// <summary>
|
||||
/// The public name of this api.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the group in witch this API is. You can also specify a custom sort order using the following
|
||||
/// format: <code>order:name</code>. Everything before the first <c>:</c> will be removed but kept for
|
||||
/// th alphabetical ordering.
|
||||
/// </summary>
|
||||
public string? Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ApiDefinitionAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the api that will be used on the documentation page.</param>
|
||||
public ApiDefinitionAttribute(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,10 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// An attribute to inform that the property is computed automatically and can't be assigned manually.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ComputedAttribute : NotMergeableAttribute { }
|
||||
}
|
||||
namespace Kyoo.Abstractions.Models.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// An attribute to inform that the property is computed automatically and can't be assigned manually.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ComputedAttribute : NotMergeableAttribute { }
|
||||
|
||||
@@ -18,37 +18,36 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Attributes
|
||||
namespace Kyoo.Abstractions.Models.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// The targeted relation can be loaded.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class LoadableRelationAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The targeted relation can be loaded.
|
||||
/// The name of the field containing the related resource's ID.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class LoadableRelationAttribute : Attribute
|
||||
public string? RelationID { get; }
|
||||
|
||||
public string? Sql { get; set; }
|
||||
|
||||
public string? On { get; set; }
|
||||
|
||||
public string? Projected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="LoadableRelationAttribute"/>.
|
||||
/// </summary>
|
||||
public LoadableRelationAttribute() { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="LoadableRelationAttribute"/> with a baking relationID field.
|
||||
/// </summary>
|
||||
/// <param name="relationID">The name of the RelationID field.</param>
|
||||
public LoadableRelationAttribute(string relationID)
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the field containing the related resource's ID.
|
||||
/// </summary>
|
||||
public string? RelationID { get; }
|
||||
|
||||
public string? Sql { get; set; }
|
||||
|
||||
public string? On { get; set; }
|
||||
|
||||
public string? Projected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="LoadableRelationAttribute"/>.
|
||||
/// </summary>
|
||||
public LoadableRelationAttribute() { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="LoadableRelationAttribute"/> with a baking relationID field.
|
||||
/// </summary>
|
||||
/// <param name="relationID">The name of the RelationID field.</param>
|
||||
public LoadableRelationAttribute(string relationID)
|
||||
{
|
||||
RelationID = relationID;
|
||||
}
|
||||
RelationID = relationID;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,23 +18,22 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Attributes
|
||||
namespace Kyoo.Abstractions.Models.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// Specify that a property can't be merged.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class NotMergeableAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// An interface with a method called when this object is merged.
|
||||
/// </summary>
|
||||
public interface IOnMerge
|
||||
{
|
||||
/// <summary>
|
||||
/// Specify that a property can't be merged.
|
||||
/// This function is called after the object has been merged.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class NotMergeableAttribute : Attribute { }
|
||||
|
||||
/// <summary>
|
||||
/// An interface with a method called when this object is merged.
|
||||
/// </summary>
|
||||
public interface IOnMerge
|
||||
{
|
||||
/// <summary>
|
||||
/// This function is called after the object has been merged.
|
||||
/// </summary>
|
||||
/// <param name="merged">The object that has been merged with this.</param>
|
||||
void OnMerge(object merged);
|
||||
}
|
||||
/// <param name="merged">The object that has been merged with this.</param>
|
||||
void OnMerge(object merged);
|
||||
}
|
||||
|
||||
@@ -21,68 +21,67 @@ using Kyoo.Abstractions.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Permissions
|
||||
namespace Kyoo.Abstractions.Models.Permissions;
|
||||
|
||||
/// <summary>
|
||||
/// Specify one part of a permissions needed for the API (the kind or the type).
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||
public class PartialPermissionAttribute : Attribute, IFilterFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Specify one part of a permissions needed for the API (the kind or the type).
|
||||
/// The needed permission type.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||
public class PartialPermissionAttribute : Attribute, IFilterFactory
|
||||
public string? Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The needed permission kind.
|
||||
/// </summary>
|
||||
public Kind? Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The group of this permission.
|
||||
/// </summary>
|
||||
public Group Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ask a permission to run an action.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// With this attribute, you can only specify a type or a kind.
|
||||
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
|
||||
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
|
||||
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
|
||||
/// lead to unspecified behaviors.
|
||||
/// </remarks>
|
||||
/// <param name="type">The type of the action</param>
|
||||
public PartialPermissionAttribute(string type)
|
||||
{
|
||||
/// <summary>
|
||||
/// The needed permission type.
|
||||
/// </summary>
|
||||
public string? Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The needed permission kind.
|
||||
/// </summary>
|
||||
public Kind? Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The group of this permission.
|
||||
/// </summary>
|
||||
public Group? Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ask a permission to run an action.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// With this attribute, you can only specify a type or a kind.
|
||||
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
|
||||
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
|
||||
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
|
||||
/// lead to unspecified behaviors.
|
||||
/// </remarks>
|
||||
/// <param name="type">The type of the action</param>
|
||||
public PartialPermissionAttribute(string type)
|
||||
{
|
||||
Type = type.ToLower();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ask a permission to run an action.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// With this attribute, you can only specify a type or a kind.
|
||||
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
|
||||
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
|
||||
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
|
||||
/// lead to unspecified behaviors.
|
||||
/// </remarks>
|
||||
/// <param name="permission">The kind of permission needed.</param>
|
||||
public PartialPermissionAttribute(Kind permission)
|
||||
{
|
||||
Kind = permission;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||
{
|
||||
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsReusable => true;
|
||||
Type = type.ToLower();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ask a permission to run an action.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// With this attribute, you can only specify a type or a kind.
|
||||
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
|
||||
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
|
||||
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
|
||||
/// lead to unspecified behaviors.
|
||||
/// </remarks>
|
||||
/// <param name="permission">The kind of permission needed.</param>
|
||||
public PartialPermissionAttribute(Kind permission)
|
||||
{
|
||||
Kind = permission;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||
{
|
||||
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsReusable => true;
|
||||
}
|
||||
|
||||
@@ -21,107 +21,116 @@ using Kyoo.Abstractions.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Permissions
|
||||
namespace Kyoo.Abstractions.Models.Permissions;
|
||||
|
||||
/// <summary>
|
||||
/// The kind of permission needed.
|
||||
/// </summary>
|
||||
public enum Kind
|
||||
{
|
||||
/// <summary>
|
||||
/// Allow the user to read for this kind of data.
|
||||
/// </summary>
|
||||
Read,
|
||||
|
||||
/// <summary>
|
||||
/// Allow the user to write for this kind of data.
|
||||
/// </summary>
|
||||
Write,
|
||||
|
||||
/// <summary>
|
||||
/// Allow the user to create this kind of data.
|
||||
/// </summary>
|
||||
Create,
|
||||
|
||||
/// <summary>
|
||||
/// Allow the user to delete this kind of data.
|
||||
/// </summary>
|
||||
Delete,
|
||||
|
||||
/// <summary>
|
||||
/// Allow the user to play this file.
|
||||
/// </summary>
|
||||
Play,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The group of the permission.
|
||||
/// </summary>
|
||||
public enum Group
|
||||
{
|
||||
/// <summary>
|
||||
/// Default group indicating no value.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Allow all operations on basic items types.
|
||||
/// </summary>
|
||||
Overall,
|
||||
|
||||
/// <summary>
|
||||
/// Allow operation on sensitive items like libraries path, configurations and so on.
|
||||
/// </summary>
|
||||
Admin
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specify permissions needed for the API.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||
public class PermissionAttribute : Attribute, IFilterFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The needed permission as string.
|
||||
/// </summary>
|
||||
public string Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The needed permission kind.
|
||||
/// </summary>
|
||||
public Kind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The group of this permission.
|
||||
/// </summary>
|
||||
public Group Group { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Ask a permission to run an action.
|
||||
/// </summary>
|
||||
/// <param name="type">
|
||||
/// The type of the action
|
||||
/// </param>
|
||||
/// <param name="permission">
|
||||
/// The kind of permission needed.
|
||||
/// </summary>
|
||||
public enum Kind
|
||||
/// </param>
|
||||
/// <param name="group">
|
||||
/// The group of this permission (allow grouped permission like overall.read
|
||||
/// for all read permissions of this group).
|
||||
/// </param>
|
||||
public PermissionAttribute(string type, Kind permission, Group group = Group.Overall)
|
||||
{
|
||||
/// <summary>
|
||||
/// Allow the user to read for this kind of data.
|
||||
/// </summary>
|
||||
Read,
|
||||
|
||||
/// <summary>
|
||||
/// Allow the user to write for this kind of data.
|
||||
/// </summary>
|
||||
Write,
|
||||
|
||||
/// <summary>
|
||||
/// Allow the user to create this kind of data.
|
||||
/// </summary>
|
||||
Create,
|
||||
|
||||
/// <summary>
|
||||
/// Allow the user to delete this kind od data.
|
||||
/// </summary>
|
||||
Delete
|
||||
Type = type.ToLower();
|
||||
Kind = permission;
|
||||
Group = group;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The group of the permission.
|
||||
/// </summary>
|
||||
public enum Group
|
||||
/// <inheritdoc />
|
||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||
{
|
||||
/// <summary>
|
||||
/// Allow all operations on basic items types.
|
||||
/// </summary>
|
||||
Overall,
|
||||
|
||||
/// <summary>
|
||||
/// Allow operation on sensitive items like libraries path, configurations and so on.
|
||||
/// </summary>
|
||||
Admin
|
||||
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsReusable => true;
|
||||
|
||||
/// <summary>
|
||||
/// Specify permissions needed for the API.
|
||||
/// Return this permission attribute as a string.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||
public class PermissionAttribute : Attribute, IFilterFactory
|
||||
/// <returns>The string representation.</returns>
|
||||
public string AsPermissionString()
|
||||
{
|
||||
/// <summary>
|
||||
/// The needed permission as string.
|
||||
/// </summary>
|
||||
public string Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The needed permission kind.
|
||||
/// </summary>
|
||||
public Kind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The group of this permission.
|
||||
/// </summary>
|
||||
public Group Group { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Ask a permission to run an action.
|
||||
/// </summary>
|
||||
/// <param name="type">
|
||||
/// The type of the action
|
||||
/// </param>
|
||||
/// <param name="permission">
|
||||
/// The kind of permission needed.
|
||||
/// </param>
|
||||
/// <param name="group">
|
||||
/// The group of this permission (allow grouped permission like overall.read
|
||||
/// for all read permissions of this group).
|
||||
/// </param>
|
||||
public PermissionAttribute(string type, Kind permission, Group group = Group.Overall)
|
||||
{
|
||||
Type = type.ToLower();
|
||||
Kind = permission;
|
||||
Group = group;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||
{
|
||||
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsReusable => true;
|
||||
|
||||
/// <summary>
|
||||
/// Return this permission attribute as a string.
|
||||
/// </summary>
|
||||
/// <returns>The string representation.</returns>
|
||||
public string AsPermissionString()
|
||||
{
|
||||
return Type;
|
||||
}
|
||||
return Type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,13 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Permissions
|
||||
namespace Kyoo.Abstractions.Models.Permissions;
|
||||
|
||||
/// <summary>
|
||||
/// The annotated route can only be accessed by a logged in user.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||
public class UserOnlyAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The annotated route can only be accessed by a logged in user.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||
public class UserOnlyAttribute : Attribute
|
||||
{
|
||||
// TODO: Implement a Filter Attribute to make this work. For now, this attribute is only useful as documentation.
|
||||
}
|
||||
// TODO: Implement a Filter Attribute to make this work. For now, this attribute is only useful as documentation.
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Remove an property from the serialization pipeline. It will simply be skipped.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
||||
public class SerializeIgnoreAttribute : Attribute { }
|
||||
}
|
||||
@@ -17,37 +17,18 @@
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Exceptions
|
||||
namespace Kyoo.Abstractions.Models.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// An exception raised when an item already exists in the database.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class DuplicatedItemException(object? existing = null)
|
||||
: Exception("Already exists in the database.")
|
||||
{
|
||||
/// <summary>
|
||||
/// An exception raised when an item already exists in the database.
|
||||
/// The existing object.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class DuplicatedItemException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// The existing object.
|
||||
/// </summary>
|
||||
public object? Existing { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="DuplicatedItemException"/> with the default message.
|
||||
/// </summary>
|
||||
/// <param name="existing">The existing object.</param>
|
||||
public DuplicatedItemException(object? existing = null)
|
||||
: base("Already exists in the database.")
|
||||
{
|
||||
Existing = existing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The serialization constructor.
|
||||
/// </summary>
|
||||
/// <param name="info">Serialization infos</param>
|
||||
/// <param name="context">The serialization context</param>
|
||||
protected DuplicatedItemException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context) { }
|
||||
}
|
||||
public object? Existing { get; } = existing;
|
||||
}
|
||||
|
||||
@@ -17,34 +17,25 @@
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Exceptions
|
||||
namespace Kyoo.Abstractions.Models.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// An exception raised when an item could not be found.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ItemNotFoundException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// An exception raised when an item could not be found.
|
||||
/// Create a default <see cref="ItemNotFoundException"/> with no message.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ItemNotFoundException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a default <see cref="ItemNotFoundException"/> with no message.
|
||||
/// </summary>
|
||||
public ItemNotFoundException() { }
|
||||
public ItemNotFoundException()
|
||||
: base("Item not found") { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ItemNotFoundException"/> with a message
|
||||
/// </summary>
|
||||
/// <param name="message">The message of the exception</param>
|
||||
public ItemNotFoundException(string message)
|
||||
: base(message) { }
|
||||
|
||||
/// <summary>
|
||||
/// The serialization constructor
|
||||
/// </summary>
|
||||
/// <param name="info">Serialization infos</param>
|
||||
/// <param name="context">The serialization context</param>
|
||||
protected ItemNotFoundException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context) { }
|
||||
}
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ItemNotFoundException"/> with a message
|
||||
/// </summary>
|
||||
/// <param name="message">The message of the exception</param>
|
||||
public ItemNotFoundException(string message)
|
||||
: base(message) { }
|
||||
}
|
||||
|
||||
@@ -17,20 +17,15 @@
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Exceptions
|
||||
namespace Kyoo.Abstractions.Models.Exceptions;
|
||||
|
||||
[Serializable]
|
||||
public class UnauthorizedException : Exception
|
||||
{
|
||||
[Serializable]
|
||||
public class UnauthorizedException : Exception
|
||||
{
|
||||
public UnauthorizedException()
|
||||
: base("User not authenticated or token invalid.") { }
|
||||
public UnauthorizedException()
|
||||
: base("User not authenticated or token invalid.") { }
|
||||
|
||||
public UnauthorizedException(string message)
|
||||
: base(message) { }
|
||||
|
||||
protected UnauthorizedException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context) { }
|
||||
}
|
||||
public UnauthorizedException(string message)
|
||||
: base(message) { }
|
||||
}
|
||||
|
||||
@@ -16,30 +16,29 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A genre that allow one to specify categories for shows.
|
||||
/// </summary>
|
||||
public enum Genre
|
||||
{
|
||||
/// <summary>
|
||||
/// A genre that allow one to specify categories for shows.
|
||||
/// </summary>
|
||||
public enum Genre
|
||||
{
|
||||
Action,
|
||||
Adventure,
|
||||
Animation,
|
||||
Comedy,
|
||||
Crime,
|
||||
Documentary,
|
||||
Drama,
|
||||
Family,
|
||||
Fantasy,
|
||||
History,
|
||||
Horror,
|
||||
Music,
|
||||
Mystery,
|
||||
Romance,
|
||||
ScienceFiction,
|
||||
Thriller,
|
||||
War,
|
||||
Western,
|
||||
}
|
||||
Action,
|
||||
Adventure,
|
||||
Animation,
|
||||
Comedy,
|
||||
Crime,
|
||||
Documentary,
|
||||
Drama,
|
||||
Family,
|
||||
Fantasy,
|
||||
History,
|
||||
Horror,
|
||||
Music,
|
||||
Mystery,
|
||||
Romance,
|
||||
ScienceFiction,
|
||||
Thriller,
|
||||
War,
|
||||
Western,
|
||||
}
|
||||
|
||||
52
back/src/Kyoo.Abstractions/Models/Issues.cs
Normal file
52
back/src/Kyoo.Abstractions/Models/Issues.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// An issue that occured on kyoo.
|
||||
/// </summary>
|
||||
public class Issue : IAddedDate
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of issue (for example, "Scanner" if this issue was created due to scanning error).
|
||||
/// </summary>
|
||||
public string Domain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Why this issue was caused? An unique cause that can be used to identify this issue.
|
||||
/// For the scanner, a cause should be a video path.
|
||||
/// </summary>
|
||||
public string Cause { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A human readable string explaining why this issue occured.
|
||||
/// </summary>
|
||||
public string Reason { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Some extra data that could store domain-specific info.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Extra { get; set; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime AddedDate { get; set; }
|
||||
}
|
||||
@@ -16,21 +16,20 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// ID and link of an item on an external provider.
|
||||
/// </summary>
|
||||
public class MetadataId
|
||||
{
|
||||
/// <summary>
|
||||
/// ID and link of an item on an external provider.
|
||||
/// The ID of the resource on the external provider.
|
||||
/// </summary>
|
||||
public class MetadataId
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the resource on the external provider.
|
||||
/// </summary>
|
||||
public string DataId { get; set; }
|
||||
public string DataId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL of the resource on the external provider.
|
||||
/// </summary>
|
||||
public string? Link { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// The URL of the resource on the external provider.
|
||||
/// </summary>
|
||||
public string? Link { get; set; }
|
||||
}
|
||||
|
||||
@@ -20,93 +20,86 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Kyoo.Utils;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A page of resource that contains information about the pagination of resources.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of resource contained in this page.</typeparam>
|
||||
public class Page<T>
|
||||
where T : IResource
|
||||
{
|
||||
/// <summary>
|
||||
/// A page of resource that contains information about the pagination of resources.
|
||||
/// The link of the current page.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of resource contained in this page.</typeparam>
|
||||
public class Page<T>
|
||||
where T : IResource
|
||||
public string This { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The link of the first page.
|
||||
/// </summary>
|
||||
public string First { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The link of the previous page.
|
||||
/// </summary>
|
||||
public string? Previous { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The link of the next page.
|
||||
/// </summary>
|
||||
public string? Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of items in the current page.
|
||||
/// </summary>
|
||||
public int Count => Items.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The list of items in the page.
|
||||
/// </summary>
|
||||
public ICollection<T> Items { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Page{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="items">The list of items in the page.</param>
|
||||
/// <param name="this">The link of the current page.</param>
|
||||
/// <param name="previous">The link of the previous page.</param>
|
||||
/// <param name="next">The link of the next page.</param>
|
||||
/// <param name="first">The link of the first page.</param>
|
||||
public Page(ICollection<T> items, string @this, string? previous, string? next, string first)
|
||||
{
|
||||
/// <summary>
|
||||
/// The link of the current page.
|
||||
/// </summary>
|
||||
public string This { get; }
|
||||
Items = items;
|
||||
This = @this;
|
||||
Previous = previous;
|
||||
Next = next;
|
||||
First = first;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The link of the first page.
|
||||
/// </summary>
|
||||
public string First { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The link of the previous page.
|
||||
/// </summary>
|
||||
public string? Previous { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The link of the next page.
|
||||
/// </summary>
|
||||
public string? Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of items in the current page.
|
||||
/// </summary>
|
||||
public int Count => Items.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The list of items in the page.
|
||||
/// </summary>
|
||||
public ICollection<T> Items { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Page{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="items">The list of items in the page.</param>
|
||||
/// <param name="this">The link of the current page.</param>
|
||||
/// <param name="previous">The link of the previous page.</param>
|
||||
/// <param name="next">The link of the next page.</param>
|
||||
/// <param name="first">The link of the first page.</param>
|
||||
public Page(
|
||||
ICollection<T> items,
|
||||
string @this,
|
||||
string? previous,
|
||||
string? next,
|
||||
string first
|
||||
)
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Page{T}"/> and compute the urls.
|
||||
/// </summary>
|
||||
/// <param name="items">The list of items in the page.</param>
|
||||
/// <param name="url">The base url of the resources available from this page.</param>
|
||||
/// <param name="query">The list of query strings of the current page</param>
|
||||
/// <param name="limit">The number of items requested for the current page.</param>
|
||||
public Page(ICollection<T> items, string url, Dictionary<string, string> query, int limit)
|
||||
{
|
||||
Items = items;
|
||||
This = url + query.ToQueryString();
|
||||
if (items.Count > 0 && query.ContainsKey("afterID"))
|
||||
{
|
||||
Items = items;
|
||||
This = @this;
|
||||
Previous = previous;
|
||||
Next = next;
|
||||
First = first;
|
||||
query["afterID"] = items.First().Id.ToString();
|
||||
query["reverse"] = "true";
|
||||
Previous = url + query.ToQueryString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Page{T}"/> and compute the urls.
|
||||
/// </summary>
|
||||
/// <param name="items">The list of items in the page.</param>
|
||||
/// <param name="url">The base url of the resources available from this page.</param>
|
||||
/// <param name="query">The list of query strings of the current page</param>
|
||||
/// <param name="limit">The number of items requested for the current page.</param>
|
||||
public Page(ICollection<T> items, string url, Dictionary<string, string> query, int limit)
|
||||
query.Remove("reverse");
|
||||
if (items.Count == limit && limit > 0)
|
||||
{
|
||||
Items = items;
|
||||
This = url + query.ToQueryString();
|
||||
if (items.Count > 0 && query.ContainsKey("afterID"))
|
||||
{
|
||||
query["afterID"] = items.First().Id.ToString();
|
||||
query["reverse"] = "true";
|
||||
Previous = url + query.ToQueryString();
|
||||
}
|
||||
query.Remove("reverse");
|
||||
if (items.Count == limit && limit > 0)
|
||||
{
|
||||
query["afterID"] = items.Last().Id.ToString();
|
||||
Next = url + query.ToQueryString();
|
||||
}
|
||||
query.Remove("afterID");
|
||||
First = url + query.ToQueryString();
|
||||
query["afterID"] = items.Last().Id.ToString();
|
||||
Next = url + query.ToQueryString();
|
||||
}
|
||||
query.Remove("afterID");
|
||||
First = url + query.ToQueryString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,27 +19,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Kyoo.Models;
|
||||
|
||||
public class Patch<T> : Dictionary<string, JToken>
|
||||
public class Patch<T> : Dictionary<string, JsonDocument>
|
||||
where T : class, IResource
|
||||
{
|
||||
public Guid? Id => this.GetValueOrDefault(nameof(IResource.Id))?.ToObject<Guid>();
|
||||
public Guid? Id => this.GetValueOrDefault(nameof(IResource.Id))?.Deserialize<Guid>();
|
||||
|
||||
public string? Slug => this.GetValueOrDefault(nameof(IResource.Slug))?.ToObject<string>();
|
||||
public string? Slug => this.GetValueOrDefault(nameof(IResource.Slug))?.Deserialize<string>();
|
||||
|
||||
public T Apply(T current)
|
||||
{
|
||||
foreach ((string property, JToken value) in this)
|
||||
foreach ((string property, JsonDocument value) in this)
|
||||
{
|
||||
PropertyInfo prop = typeof(T).GetProperty(
|
||||
property,
|
||||
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
|
||||
)!;
|
||||
prop.SetValue(current, value.ToObject(prop.PropertyType));
|
||||
prop.SetValue(current, value.Deserialize(prop.PropertyType));
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A role a person played for a show. It can be an actor, musician, voice actor, director, writer...
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class is not serialized like other classes.
|
||||
/// Based on the <see cref="ForPeople"/> field, it is serialized like
|
||||
/// a show with two extra fields (<see cref="Role"/> and <see cref="Type"/>).
|
||||
/// </remarks>
|
||||
public class PeopleRole : IResource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Slug => ForPeople ? Show!.Slug : People.Slug;
|
||||
|
||||
/// <summary>
|
||||
/// Should this role be used as a Show substitute (the value is <c>true</c>) or
|
||||
/// as a People substitute (the value is <c>false</c>).
|
||||
/// </summary>
|
||||
public bool ForPeople { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the People playing the role.
|
||||
/// </summary>
|
||||
public Guid PeopleID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The people that played this role.
|
||||
/// </summary>
|
||||
public People People { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Show where the People playing in.
|
||||
/// </summary>
|
||||
public Guid? ShowID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The show where the People played in.
|
||||
/// </summary>
|
||||
public Show? Show { get; set; }
|
||||
|
||||
public Guid? MovieID { get; set; }
|
||||
|
||||
public Movie? Movie { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of work the person has done for the show.
|
||||
/// That can be something like "Actor", "Writer", "Music", "Voice Actor"...
|
||||
/// </summary>
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The role the People played.
|
||||
/// This is mostly used to inform witch character was played for actor and voice actors.
|
||||
/// </summary>
|
||||
public string Role { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -19,74 +19,72 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A class representing collections of <see cref="Show"/>.
|
||||
/// </summary>
|
||||
public class Collection : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, ILibraryItem
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<Collection>.By(nameof(Collection.Name));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A class representing collections of <see cref="Show"/>.
|
||||
/// The name of this collection.
|
||||
/// </summary>
|
||||
public class Collection : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, ILibraryItem
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The description of this collection.
|
||||
/// </summary>
|
||||
public string? Overview { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Poster { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Thumbnail { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Logo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of movies contained in this collection.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ICollection<Movie>? Movies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of shows contained in this collection.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ICollection<Show>? Shows { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
|
||||
public Collection() { }
|
||||
|
||||
[JsonConstructor]
|
||||
public Collection(string name)
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<Collection>.By(nameof(Collection.Name));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this collection.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The description of this collection.
|
||||
/// </summary>
|
||||
public string? Overview { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Poster { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Thumbnail { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Logo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of movies contained in this collection.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<Movie>? Movies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of shows contained in this collection.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<Show>? Shows { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
|
||||
public Collection() { }
|
||||
|
||||
[JsonConstructor]
|
||||
public Collection(string name)
|
||||
if (name != null)
|
||||
{
|
||||
if (name != null)
|
||||
{
|
||||
Slug = Utility.ToSlug(name);
|
||||
Name = name;
|
||||
}
|
||||
Slug = Utility.ToSlug(name);
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,167 +20,161 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using EntityFrameworkCore.Projectables;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A class to represent a single show's episode.
|
||||
/// </summary>
|
||||
public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, INews
|
||||
{
|
||||
/// <summary>
|
||||
/// A class to represent a single show's episode.
|
||||
/// </summary>
|
||||
public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, INews
|
||||
// Use absolute numbers by default and fallback to season/episodes if it does not exists.
|
||||
public static Sort DefaultSort =>
|
||||
new Sort<Episode>.Conglomerate(
|
||||
new Sort<Episode>.By(x => x.AbsoluteNumber),
|
||||
new Sort<Episode>.By(x => x.SeasonNumber),
|
||||
new Sort<Episode>.By(x => x.EpisodeNumber)
|
||||
);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[Computed]
|
||||
[MaxLength(256)]
|
||||
public string Slug
|
||||
{
|
||||
// Use absolute numbers by default and fallback to season/episodes if it does not exists.
|
||||
public static Sort DefaultSort =>
|
||||
new Sort<Episode>.Conglomerate(
|
||||
new Sort<Episode>.By(x => x.AbsoluteNumber),
|
||||
new Sort<Episode>.By(x => x.SeasonNumber),
|
||||
new Sort<Episode>.By(x => x.EpisodeNumber)
|
||||
);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[Computed]
|
||||
[MaxLength(256)]
|
||||
public string Slug
|
||||
get
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ShowSlug != null || Show?.Slug != null)
|
||||
return GetSlug(
|
||||
ShowSlug ?? Show!.Slug,
|
||||
SeasonNumber,
|
||||
EpisodeNumber,
|
||||
AbsoluteNumber
|
||||
);
|
||||
return GetSlug(ShowId.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
||||
}
|
||||
[UsedImplicitly]
|
||||
private set
|
||||
{
|
||||
Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)e(?<episode>\d+)");
|
||||
if (ShowSlug != null || Show?.Slug != null)
|
||||
return GetSlug(ShowSlug ?? Show!.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
||||
return GetSlug(ShowId.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
||||
}
|
||||
private set
|
||||
{
|
||||
Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)e(?<episode>\d+)");
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
ShowSlug = match.Groups["show"].Value;
|
||||
SeasonNumber = int.Parse(match.Groups["season"].Value);
|
||||
EpisodeNumber = int.Parse(match.Groups["episode"].Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
match = Regex.Match(value, @"(?<show>.+)-(?<absolute>\d+)");
|
||||
if (match.Success)
|
||||
{
|
||||
ShowSlug = match.Groups["show"].Value;
|
||||
SeasonNumber = int.Parse(match.Groups["season"].Value);
|
||||
EpisodeNumber = int.Parse(match.Groups["episode"].Value);
|
||||
AbsoluteNumber = int.Parse(match.Groups["absolute"].Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
match = Regex.Match(value, @"(?<show>.+)-(?<absolute>\d+)");
|
||||
if (match.Success)
|
||||
{
|
||||
ShowSlug = match.Groups["show"].Value;
|
||||
AbsoluteNumber = int.Parse(match.Groups["absolute"].Value);
|
||||
}
|
||||
else
|
||||
ShowSlug = value;
|
||||
SeasonNumber = null;
|
||||
EpisodeNumber = null;
|
||||
}
|
||||
ShowSlug = value;
|
||||
SeasonNumber = null;
|
||||
EpisodeNumber = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public string? ShowSlug { private get; set; }
|
||||
/// <summary>
|
||||
/// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string? ShowSlug { private get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Show containing this episode.
|
||||
/// </summary>
|
||||
public Guid ShowId { get; set; }
|
||||
/// <summary>
|
||||
/// The ID of the Show containing this episode.
|
||||
/// </summary>
|
||||
public Guid ShowId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The show that contains this episode.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(ShowId))]
|
||||
public Show? Show { get; set; }
|
||||
/// <summary>
|
||||
/// The show that contains this episode.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(ShowId))]
|
||||
public Show? Show { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Season containing this episode.
|
||||
/// </summary>
|
||||
public Guid? SeasonId { get; set; }
|
||||
/// <summary>
|
||||
/// The ID of the Season containing this episode.
|
||||
/// </summary>
|
||||
public Guid? SeasonId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The season that contains this episode.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This can be null if the season is unknown and the episode is only identified
|
||||
/// by it's <see cref="AbsoluteNumber"/>.
|
||||
/// </remarks>
|
||||
[LoadableRelation(nameof(SeasonId))]
|
||||
public Season? Season { get; set; }
|
||||
/// <summary>
|
||||
/// The season that contains this episode.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This can be null if the season is unknown and the episode is only identified
|
||||
/// by it's <see cref="AbsoluteNumber"/>.
|
||||
/// </remarks>
|
||||
[LoadableRelation(nameof(SeasonId))]
|
||||
public Season? Season { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The season in witch this episode is in.
|
||||
/// </summary>
|
||||
public int? SeasonNumber { get; set; }
|
||||
/// <summary>
|
||||
/// The season in witch this episode is in.
|
||||
/// </summary>
|
||||
public int? SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of this episode in it's season.
|
||||
/// </summary>
|
||||
public int? EpisodeNumber { get; set; }
|
||||
/// <summary>
|
||||
/// The number of this episode in it's season.
|
||||
/// </summary>
|
||||
public int? EpisodeNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
|
||||
/// </summary>
|
||||
public int? AbsoluteNumber { get; set; }
|
||||
/// <summary>
|
||||
/// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
|
||||
/// </summary>
|
||||
public int? AbsoluteNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of the video file for this episode.
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
/// <summary>
|
||||
/// The path of the video file for this episode.
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of this episode.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
/// <summary>
|
||||
/// The title of this episode.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The overview of this episode.
|
||||
/// </summary>
|
||||
public string? Overview { get; set; }
|
||||
/// <summary>
|
||||
/// The overview of this episode.
|
||||
/// </summary>
|
||||
public string? Overview { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How long is this episode? (in minutes)
|
||||
/// </summary>
|
||||
public int Runtime { get; set; }
|
||||
/// <summary>
|
||||
/// How long is this episode? (in minutes)
|
||||
/// </summary>
|
||||
public int? Runtime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The release date of this episode. It can be null if unknown.
|
||||
/// </summary>
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
/// <summary>
|
||||
/// The release date of this episode. It can be null if unknown.
|
||||
/// </summary>
|
||||
public DateOnly? ReleaseDate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Poster { get; set; }
|
||||
/// <inheritdoc />
|
||||
public Image? Poster { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Thumbnail { get; set; }
|
||||
/// <inheritdoc />
|
||||
public Image? Thumbnail { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Logo { get; set; }
|
||||
/// <inheritdoc />
|
||||
public Image? Logo { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The previous episode that should be seen before viewing this one.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_PreviousEpisode), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
// language=PostgreSQL
|
||||
Sql = """
|
||||
/// <summary>
|
||||
/// The previous episode that should be seen before viewing this one.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_PreviousEpisode), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
// language=PostgreSQL
|
||||
Sql = """
|
||||
select
|
||||
pe.* -- Episode as pe
|
||||
from
|
||||
@@ -197,30 +191,28 @@ namespace Kyoo.Abstractions.Models
|
||||
pe.episode_number desc
|
||||
limit 1
|
||||
"""
|
||||
)]
|
||||
public Episode? PreviousEpisode { get; set; }
|
||||
)]
|
||||
public Episode? PreviousEpisode { get; set; }
|
||||
|
||||
private Episode? _PreviousEpisode =>
|
||||
Show!
|
||||
.Episodes!
|
||||
.OrderBy(x => x.AbsoluteNumber == null)
|
||||
.ThenByDescending(x => x.AbsoluteNumber)
|
||||
.ThenByDescending(x => x.SeasonNumber)
|
||||
.ThenByDescending(x => x.EpisodeNumber)
|
||||
.FirstOrDefault(
|
||||
x =>
|
||||
x.AbsoluteNumber < AbsoluteNumber
|
||||
|| x.SeasonNumber < SeasonNumber
|
||||
|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber < EpisodeNumber)
|
||||
);
|
||||
private Episode? _PreviousEpisode =>
|
||||
Show!
|
||||
.Episodes!.OrderBy(x => x.AbsoluteNumber == null)
|
||||
.ThenByDescending(x => x.AbsoluteNumber)
|
||||
.ThenByDescending(x => x.SeasonNumber)
|
||||
.ThenByDescending(x => x.EpisodeNumber)
|
||||
.FirstOrDefault(x =>
|
||||
x.AbsoluteNumber < AbsoluteNumber
|
||||
|| x.SeasonNumber < SeasonNumber
|
||||
|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber < EpisodeNumber)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// The next episode to watch after this one.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_NextEpisode), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
// language=PostgreSQL
|
||||
Sql = """
|
||||
/// <summary>
|
||||
/// The next episode to watch after this one.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_NextEpisode), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
// language=PostgreSQL
|
||||
Sql = """
|
||||
select
|
||||
ne.* -- Episode as ne
|
||||
from
|
||||
@@ -237,78 +229,71 @@ namespace Kyoo.Abstractions.Models
|
||||
ne.episode_number
|
||||
limit 1
|
||||
"""
|
||||
)]
|
||||
public Episode? NextEpisode { get; set; }
|
||||
)]
|
||||
public Episode? NextEpisode { get; set; }
|
||||
|
||||
private Episode? _NextEpisode =>
|
||||
Show!
|
||||
.Episodes!
|
||||
.OrderBy(x => x.AbsoluteNumber)
|
||||
.ThenBy(x => x.SeasonNumber)
|
||||
.ThenBy(x => x.EpisodeNumber)
|
||||
.FirstOrDefault(
|
||||
x =>
|
||||
x.AbsoluteNumber > AbsoluteNumber
|
||||
|| x.SeasonNumber > SeasonNumber
|
||||
|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber > EpisodeNumber)
|
||||
);
|
||||
private Episode? _NextEpisode =>
|
||||
Show!
|
||||
.Episodes!.OrderBy(x => x.AbsoluteNumber)
|
||||
.ThenBy(x => x.SeasonNumber)
|
||||
.ThenBy(x => x.EpisodeNumber)
|
||||
.FirstOrDefault(x =>
|
||||
x.AbsoluteNumber > AbsoluteNumber
|
||||
|| x.SeasonNumber > SeasonNumber
|
||||
|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber > EpisodeNumber)
|
||||
);
|
||||
|
||||
[SerializeIgnore]
|
||||
public ICollection<EpisodeWatchStatus>? Watched { get; set; }
|
||||
[JsonIgnore]
|
||||
public ICollection<EpisodeWatchStatus>? Watched { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata of what an user as started/planned to watch.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
Sql = "episode_watch_status",
|
||||
On = "episode_id = \"this\".id and \"relation\".user_id = [current_user]"
|
||||
)]
|
||||
public EpisodeWatchStatus? WatchStatus { get; set; }
|
||||
/// <summary>
|
||||
/// Metadata of what an user as started/planned to watch.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
Sql = "episode_watch_status",
|
||||
On = "episode_id = \"this\".id and \"relation\".user_id = [current_user]"
|
||||
)]
|
||||
public EpisodeWatchStatus? WatchStatus { get; set; }
|
||||
|
||||
// There is a global query filter to filter by user so we just need to do single.
|
||||
private EpisodeWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
|
||||
// There is a global query filter to filter by user so we just need to do single.
|
||||
private EpisodeWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Links to watch this episode.
|
||||
/// </summary>
|
||||
public VideoLinks Links =>
|
||||
new()
|
||||
{
|
||||
Direct = $"/video/episode/{Slug}/direct",
|
||||
Hls = $"/video/episode/{Slug}/master.m3u8",
|
||||
};
|
||||
/// <summary>
|
||||
/// Links to watch this episode.
|
||||
/// </summary>
|
||||
public VideoLinks Links =>
|
||||
new() { Direct = $"/episode/{Slug}/direct", Hls = $"/episode/{Slug}/master.m3u8", };
|
||||
|
||||
/// <summary>
|
||||
/// Get the slug of an episode.
|
||||
/// </summary>
|
||||
/// <param name="showSlug">The slug of the show. It can't be null.</param>
|
||||
/// <param name="seasonNumber">
|
||||
/// The season in which the episode is.
|
||||
/// If this is a movie or if the episode should be referred by it's absolute number, set this to null.
|
||||
/// </param>
|
||||
/// <param name="episodeNumber">
|
||||
/// The number of the episode in it's season.
|
||||
/// If this is a movie or if the episode should be referred by it's absolute number, set this to null.
|
||||
/// </param>
|
||||
/// <param name="absoluteNumber">
|
||||
/// The absolute number of this show.
|
||||
/// If you don't know it or this is a movie, use null
|
||||
/// </param>
|
||||
/// <returns>The slug corresponding to the given arguments</returns>
|
||||
public static string GetSlug(
|
||||
string showSlug,
|
||||
int? seasonNumber,
|
||||
int? episodeNumber,
|
||||
int? absoluteNumber = null
|
||||
)
|
||||
/// <summary>
|
||||
/// Get the slug of an episode.
|
||||
/// </summary>
|
||||
/// <param name="showSlug">The slug of the show. It can't be null.</param>
|
||||
/// <param name="seasonNumber">
|
||||
/// The season in which the episode is.
|
||||
/// If this is a movie or if the episode should be referred by it's absolute number, set this to null.
|
||||
/// </param>
|
||||
/// <param name="episodeNumber">
|
||||
/// The number of the episode in it's season.
|
||||
/// If this is a movie or if the episode should be referred by it's absolute number, set this to null.
|
||||
/// </param>
|
||||
/// <param name="absoluteNumber">
|
||||
/// The absolute number of this show.
|
||||
/// If you don't know it or this is a movie, use null
|
||||
/// </param>
|
||||
/// <returns>The slug corresponding to the given arguments</returns>
|
||||
public static string GetSlug(
|
||||
string showSlug,
|
||||
int? seasonNumber,
|
||||
int? episodeNumber,
|
||||
int? absoluteNumber = null
|
||||
)
|
||||
{
|
||||
return seasonNumber switch
|
||||
{
|
||||
return seasonNumber switch
|
||||
{
|
||||
null when absoluteNumber == null => showSlug,
|
||||
null => $"{showSlug}-{absoluteNumber}",
|
||||
_ => $"{showSlug}-s{seasonNumber}e{episodeNumber}"
|
||||
};
|
||||
}
|
||||
null when absoluteNumber == null => showSlug,
|
||||
null => $"{showSlug}-{absoluteNumber}",
|
||||
_ => $"{showSlug}-s{seasonNumber}e{episodeNumber}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,16 +18,15 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// An interface applied to resources.
|
||||
/// </summary>
|
||||
public interface IAddedDate
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface applied to resources.
|
||||
/// The date at which this resource was added to kyoo.
|
||||
/// </summary>
|
||||
public interface IAddedDate
|
||||
{
|
||||
/// <summary>
|
||||
/// The date at which this resource was added to kyoo.
|
||||
/// </summary>
|
||||
public DateTime AddedDate { get; set; }
|
||||
}
|
||||
public DateTime AddedDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -18,16 +18,15 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// An interface applied to resources containing external metadata.
|
||||
/// </summary>
|
||||
public interface IMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface applied to resources containing external metadata.
|
||||
/// The link to metadata providers that this show has. See <see cref="MetadataId"/> for more information.
|
||||
/// </summary>
|
||||
public interface IMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The link to metadata providers that this show has. See <see cref="MetadataId"/> for more information.
|
||||
/// </summary>
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; }
|
||||
}
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; }
|
||||
}
|
||||
|
||||
@@ -20,31 +20,30 @@ using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// An interface to represent a resource that can be retrieved from the database.
|
||||
/// </summary>
|
||||
public interface IResource : IQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface to represent a resource that can be retrieved from the database.
|
||||
/// A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
|
||||
/// </summary>
|
||||
public interface IResource : IQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You don't need to specify an ID manually when creating a new resource,
|
||||
/// this field is automatically assigned by the <see cref="IRepository{T}"/>.
|
||||
/// </remarks>
|
||||
public Guid Id { get; set; }
|
||||
/// <remarks>
|
||||
/// You don't need to specify an ID manually when creating a new resource,
|
||||
/// this field is automatically assigned by the <see cref="IRepository{T}"/>.
|
||||
/// </remarks>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A human-readable identifier that can be used instead of an ID.
|
||||
/// A slug must be unique for a type of resource but it can be changed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There is no setter for a slug since it can be computed from other fields.
|
||||
/// For example, a season slug is {ShowSlug}-s{SeasonNumber}.
|
||||
/// </remarks>
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; }
|
||||
}
|
||||
/// <summary>
|
||||
/// A human-readable identifier that can be used instead of an ID.
|
||||
/// A slug must be unique for a type of resource but it can be changed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There is no setter for a slug since it can be computed from other fields.
|
||||
/// For example, a season slug is {ShowSlug}-s{SeasonNumber}.
|
||||
/// </remarks>
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; }
|
||||
}
|
||||
|
||||
@@ -20,108 +20,105 @@ using System;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// An interface representing items that contains images (like posters, thumbnails, logo, banners...)
|
||||
/// </summary>
|
||||
public interface IThumbnails
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface representing items that contains images (like posters, thumbnails, logo, banners...)
|
||||
/// A poster is a 2/3 format image with the cover of the resource.
|
||||
/// </summary>
|
||||
public interface IThumbnails
|
||||
{
|
||||
/// <summary>
|
||||
/// A poster is a 2/3 format image with the cover of the resource.
|
||||
/// </summary>
|
||||
public Image? Poster { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A thumbnail is a 16/9 format image, it could ether be used as a background or as a preview but it usually
|
||||
/// is not an official image.
|
||||
/// </summary>
|
||||
public Image? Thumbnail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A logo is a small image representing the resource.
|
||||
/// </summary>
|
||||
public Image? Logo { get; set; }
|
||||
}
|
||||
|
||||
[TypeConverter(typeof(ImageConvertor))]
|
||||
[SqlFirstColumn(nameof(Source))]
|
||||
public class Image
|
||||
{
|
||||
/// <summary>
|
||||
/// The original image from another server.
|
||||
/// </summary>
|
||||
public string Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A hash to display as placeholder while the image is loading.
|
||||
/// </summary>
|
||||
[MaxLength(32)]
|
||||
public string Blurhash { get; set; }
|
||||
|
||||
public Image() { }
|
||||
|
||||
[JsonConstructor]
|
||||
public Image(string source, string? blurhash = null)
|
||||
{
|
||||
Source = source;
|
||||
Blurhash = blurhash ?? "000000";
|
||||
}
|
||||
|
||||
public class ImageConvertor : TypeConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
if (sourceType == typeof(string))
|
||||
return true;
|
||||
return base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ConvertFrom(
|
||||
ITypeDescriptorContext? context,
|
||||
CultureInfo? culture,
|
||||
object value
|
||||
)
|
||||
{
|
||||
if (value is not string source)
|
||||
return base.ConvertFrom(context, culture, value)!;
|
||||
return new Image(source);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvertTo(
|
||||
ITypeDescriptorContext? context,
|
||||
Type? destinationType
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
public Image? Poster { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The quality of an image
|
||||
/// A thumbnail is a 16/9 format image, it could ether be used as a background or as a preview but it usually
|
||||
/// is not an official image.
|
||||
/// </summary>
|
||||
public enum ImageQuality
|
||||
public Image? Thumbnail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A logo is a small image representing the resource.
|
||||
/// </summary>
|
||||
public Image? Logo { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ImageConvertor))]
|
||||
[SqlFirstColumn(nameof(Source))]
|
||||
public class Image
|
||||
{
|
||||
/// <summary>
|
||||
/// The original image from another server.
|
||||
/// </summary>
|
||||
public string Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A hash to display as placeholder while the image is loading.
|
||||
/// </summary>
|
||||
[MaxLength(32)]
|
||||
public string Blurhash { get; set; }
|
||||
|
||||
public Image() { }
|
||||
|
||||
[JsonConstructor]
|
||||
public Image(string source, string? blurhash = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Small
|
||||
/// </summary>
|
||||
Low,
|
||||
Source = source;
|
||||
Blurhash = blurhash ?? "000000";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Medium
|
||||
/// </summary>
|
||||
Medium,
|
||||
public class ImageConvertor : JsonConverter<Image>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Image? Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options
|
||||
)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.String && reader.GetString() is string source)
|
||||
return new Image(source);
|
||||
using JsonDocument document = JsonDocument.ParseValue(ref reader);
|
||||
return document.RootElement.Deserialize<Image>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Large
|
||||
/// </summary>
|
||||
High,
|
||||
/// <inheritdoc />
|
||||
public override void Write(
|
||||
Utf8JsonWriter writer,
|
||||
Image value,
|
||||
JsonSerializerOptions options
|
||||
)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("source", value.Source);
|
||||
writer.WriteString("blurhash", value.Blurhash);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The quality of an image
|
||||
/// </summary>
|
||||
public enum ImageQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// Small
|
||||
/// </summary>
|
||||
Low,
|
||||
|
||||
/// <summary>
|
||||
/// Medium
|
||||
/// </summary>
|
||||
Medium,
|
||||
|
||||
/// <summary>
|
||||
/// Large
|
||||
/// </summary>
|
||||
High,
|
||||
}
|
||||
|
||||
65
back/src/Kyoo.Abstractions/Models/Resources/JwtToken.cs
Normal file
65
back/src/Kyoo.Abstractions/Models/Resources/JwtToken.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A container representing the response of a login or token refresh.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="JwtToken"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="accessToken">The access token used to authorize requests.</param>
|
||||
/// <param name="refreshToken">The refresh token to retrieve a new access token.</param>
|
||||
/// <param name="expireIn">When the access token will expire.</param>
|
||||
public class JwtToken(string accessToken, string refreshToken, TimeSpan expireIn)
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of this token (always a Bearer).
|
||||
/// </summary>
|
||||
[JsonPropertyName("token_type")]
|
||||
public string TokenType => "Bearer";
|
||||
|
||||
/// <summary>
|
||||
/// The access token used to authorize requests.
|
||||
/// </summary>
|
||||
[JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; } = accessToken;
|
||||
|
||||
/// <summary>
|
||||
/// The refresh token used to retrieve a new access/refresh token when the access token has expired.
|
||||
/// </summary>
|
||||
[JsonPropertyName("refresh_token")]
|
||||
public string RefreshToken { get; set; } = refreshToken;
|
||||
|
||||
/// <summary>
|
||||
/// When the access token will expire. After this time, the refresh token should be used to retrieve.
|
||||
/// a new token.cs
|
||||
/// </summary>
|
||||
[JsonPropertyName("expire_in")]
|
||||
public TimeSpan ExpireIn => ExpireAt.Subtract(DateTime.UtcNow);
|
||||
|
||||
/// <summary>
|
||||
/// The exact date at which the access token will expire.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expire_at")]
|
||||
public DateTime ExpireAt { get; set; } = DateTime.UtcNow + expireIn;
|
||||
}
|
||||
@@ -19,182 +19,170 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using EntityFrameworkCore.Projectables;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A series or a movie.
|
||||
/// </summary>
|
||||
public class Movie
|
||||
: IQuery,
|
||||
IResource,
|
||||
IMetadata,
|
||||
IThumbnails,
|
||||
IAddedDate,
|
||||
ILibraryItem,
|
||||
INews,
|
||||
IWatchlist
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<Movie>.By(x => x.Name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A series or a movie.
|
||||
/// The title of this show.
|
||||
/// </summary>
|
||||
public class Movie
|
||||
: IQuery,
|
||||
IResource,
|
||||
IMetadata,
|
||||
IOnMerge,
|
||||
IThumbnails,
|
||||
IAddedDate,
|
||||
ILibraryItem,
|
||||
INews,
|
||||
IWatchlist
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A catchphrase for this movie.
|
||||
/// </summary>
|
||||
public string? Tagline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of alternative titles of this show.
|
||||
/// </summary>
|
||||
public string[] Aliases { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// The path of the movie video file.
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The summary of this show.
|
||||
/// </summary>
|
||||
public string? Overview { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of tags that match this movie.
|
||||
/// </summary>
|
||||
public string[] Tags { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The list of genres (themes) this show has.
|
||||
/// </summary>
|
||||
public List<Genre> Genres { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Is this show airing, not aired yet or finished?
|
||||
/// </summary>
|
||||
public Status Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How well this item is rated? (from 0 to 100).
|
||||
/// </summary>
|
||||
public int Rating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How long is this movie? (in minutes)
|
||||
/// </summary>
|
||||
public int? Runtime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date this movie aired.
|
||||
/// </summary>
|
||||
public DateOnly? AirDate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Poster { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Thumbnail { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Logo { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
[Column("air_date")]
|
||||
public DateOnly? StartAir => AirDate;
|
||||
|
||||
[JsonIgnore]
|
||||
[Column("air_date")]
|
||||
public DateOnly? EndAir => AirDate;
|
||||
|
||||
/// <summary>
|
||||
/// A video of a few minutes that tease the content.
|
||||
/// </summary>
|
||||
public string? Trailer { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Studio that made this show.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Guid? StudioId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Studio that made this show.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(StudioId))]
|
||||
public Studio? Studio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of collections that contains this show.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ICollection<Collection>? Collections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Links to watch this movie.
|
||||
/// </summary>
|
||||
public VideoLinks Links =>
|
||||
new() { Direct = $"/movie/{Slug}/direct", Hls = $"/movie/{Slug}/master.m3u8", };
|
||||
|
||||
[JsonIgnore]
|
||||
public ICollection<MovieWatchStatus>? Watched { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata of what an user as started/planned to watch.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
Sql = "movie_watch_status",
|
||||
On = "movie_id = \"this\".id and \"relation\".user_id = [current_user]"
|
||||
)]
|
||||
public MovieWatchStatus? WatchStatus { get; set; }
|
||||
|
||||
// There is a global query filter to filter by user so we just need to do single.
|
||||
private MovieWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
|
||||
|
||||
public Movie() { }
|
||||
|
||||
[JsonConstructor]
|
||||
public Movie(string name)
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<Movie>.By(x => x.Name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of this show.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A catchphrase for this movie.
|
||||
/// </summary>
|
||||
public string? Tagline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of alternative titles of this show.
|
||||
/// </summary>
|
||||
public string[] Aliases { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// The path of the movie video file.
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The summary of this show.
|
||||
/// </summary>
|
||||
public string? Overview { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of tags that match this movie.
|
||||
/// </summary>
|
||||
public string[] Tags { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// The list of genres (themes) this show has.
|
||||
/// </summary>
|
||||
public Genre[] Genres { get; set; } = Array.Empty<Genre>();
|
||||
|
||||
/// <summary>
|
||||
/// Is this show airing, not aired yet or finished?
|
||||
/// </summary>
|
||||
public Status Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How well this item is rated? (from 0 to 100).
|
||||
/// </summary>
|
||||
public int Rating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How long is this movie? (in minutes)
|
||||
/// </summary>
|
||||
public int Runtime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date this movie aired.
|
||||
/// </summary>
|
||||
public DateTime? AirDate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Poster { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Thumbnail { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Logo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A video of a few minutes that tease the content.
|
||||
/// </summary>
|
||||
public string? Trailer { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Studio that made this show.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Guid? StudioId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Studio that made this show.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(StudioId))]
|
||||
public Studio? Studio { get; set; }
|
||||
|
||||
// /// <summary>
|
||||
// /// The list of people that made this show.
|
||||
// /// </summary>
|
||||
// [SerializeIgnore] public ICollection<PeopleRole>? People { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of collections that contains this show.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<Collection>? Collections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Links to watch this movie.
|
||||
/// </summary>
|
||||
public VideoLinks Links =>
|
||||
new()
|
||||
{
|
||||
Direct = $"/video/movie/{Slug}/direct",
|
||||
Hls = $"/video/movie/{Slug}/master.m3u8",
|
||||
};
|
||||
|
||||
[SerializeIgnore]
|
||||
public ICollection<MovieWatchStatus>? Watched { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata of what an user as started/planned to watch.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
Sql = "movie_watch_status",
|
||||
On = "movie_id = \"this\".id and \"relation\".user_id = [current_user]"
|
||||
)]
|
||||
public MovieWatchStatus? WatchStatus { get; set; }
|
||||
|
||||
// There is a global query filter to filter by user so we just need to do single.
|
||||
private MovieWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnMerge(object merged)
|
||||
if (name != null)
|
||||
{
|
||||
// if (People != null)
|
||||
// {
|
||||
// foreach (PeopleRole link in People)
|
||||
// link.Movie = this;
|
||||
// }
|
||||
}
|
||||
|
||||
public Movie() { }
|
||||
|
||||
[JsonConstructor]
|
||||
public Movie(string name)
|
||||
{
|
||||
if (name != null)
|
||||
{
|
||||
Slug = Utility.ToSlug(name);
|
||||
Name = name;
|
||||
}
|
||||
Slug = Utility.ToSlug(name);
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// An actor, voice actor, writer, animator, somebody who worked on a <see cref="Show"/>.
|
||||
/// </summary>
|
||||
[Table("people")]
|
||||
public class People : IQuery, IResource, IMetadata, IThumbnails
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<People>.By(x => x.Name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this person.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Poster { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Thumbnail { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Logo { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The list of roles this person has played in. See <see cref="PeopleRole"/> for more information.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<PeopleRole>? Roles { get; set; }
|
||||
|
||||
public People() { }
|
||||
|
||||
[JsonConstructor]
|
||||
public People(string name)
|
||||
{
|
||||
if (name != null)
|
||||
{
|
||||
Slug = Utility.ToSlug(name);
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,121 +20,119 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using EntityFrameworkCore.Projectables;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A season of a <see cref="Show"/>.
|
||||
/// </summary>
|
||||
public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate
|
||||
{
|
||||
/// <summary>
|
||||
/// A season of a <see cref="Show"/>.
|
||||
/// </summary>
|
||||
public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate
|
||||
public static Sort DefaultSort => new Sort<Season>.By(x => x.SeasonNumber);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[Computed]
|
||||
[MaxLength(256)]
|
||||
public string Slug
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<Season>.By(x => x.SeasonNumber);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[Computed]
|
||||
[MaxLength(256)]
|
||||
public string Slug
|
||||
get
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ShowSlug == null && Show == null)
|
||||
return $"{ShowId}-s{SeasonNumber}";
|
||||
return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}";
|
||||
}
|
||||
[UsedImplicitly]
|
||||
[NotNull]
|
||||
private set
|
||||
{
|
||||
Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)");
|
||||
|
||||
if (!match.Success)
|
||||
throw new ArgumentException(
|
||||
"Invalid season slug. Format: {showSlug}-s{seasonNumber}"
|
||||
);
|
||||
ShowSlug = match.Groups["show"].Value;
|
||||
SeasonNumber = int.Parse(match.Groups["season"].Value);
|
||||
}
|
||||
if (ShowSlug == null && Show == null)
|
||||
return $"{ShowId}-s{SeasonNumber}";
|
||||
return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}";
|
||||
}
|
||||
private set
|
||||
{
|
||||
Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)");
|
||||
|
||||
/// <summary>
|
||||
/// The slug of the Show that contain this episode. If this is not set, this season is ill-formed.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public string? ShowSlug { private get; set; }
|
||||
if (!match.Success)
|
||||
throw new ArgumentException(
|
||||
"Invalid season slug. Format: {showSlug}-s{seasonNumber}"
|
||||
);
|
||||
ShowSlug = match.Groups["show"].Value;
|
||||
SeasonNumber = int.Parse(match.Groups["season"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Show containing this season.
|
||||
/// </summary>
|
||||
public Guid ShowId { get; set; }
|
||||
/// <summary>
|
||||
/// The slug of the Show that contain this episode. If this is not set, this season is ill-formed.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string? ShowSlug { private get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The show that contains this season.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(ShowId))]
|
||||
public Show? Show { get; set; }
|
||||
/// <summary>
|
||||
/// The ID of the Show containing this season.
|
||||
/// </summary>
|
||||
public Guid ShowId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of this season. This can be set to 0 to indicate specials.
|
||||
/// </summary>
|
||||
public int SeasonNumber { get; set; }
|
||||
/// <summary>
|
||||
/// The show that contains this season.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(ShowId))]
|
||||
public Show? Show { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The title of this season.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
/// <summary>
|
||||
/// The number of this season. This can be set to 0 to indicate specials.
|
||||
/// </summary>
|
||||
public int SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A quick overview of this season.
|
||||
/// </summary>
|
||||
public string? Overview { get; set; }
|
||||
/// <summary>
|
||||
/// The title of this season.
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The starting air date of this season.
|
||||
/// </summary>
|
||||
public DateTime? StartDate { get; set; }
|
||||
/// <summary>
|
||||
/// A quick overview of this season.
|
||||
/// </summary>
|
||||
public string? Overview { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
/// <summary>
|
||||
/// The starting air date of this season.
|
||||
/// </summary>
|
||||
public DateOnly? StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ending date of this season.
|
||||
/// </summary>
|
||||
public DateTime? EndDate { get; set; }
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Poster { get; set; }
|
||||
/// <summary>
|
||||
/// The ending date of this season.
|
||||
/// </summary>
|
||||
public DateOnly? EndDate { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Thumbnail { get; set; }
|
||||
/// <inheritdoc />
|
||||
public Image? Poster { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Logo { get; set; }
|
||||
/// <inheritdoc />
|
||||
public Image? Thumbnail { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
/// <inheritdoc />
|
||||
public Image? Logo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of episodes that this season contains.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<Episode>? Episodes { get; set; }
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The number of episodes in this season.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_EpisodesCount), OnlyOnInclude = true)]
|
||||
[NotMapped]
|
||||
[LoadableRelation(
|
||||
// language=PostgreSQL
|
||||
Projected = """
|
||||
/// <summary>
|
||||
/// The list of episodes that this season contains.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ICollection<Episode>? Episodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of episodes in this season.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_EpisodesCount), OnlyOnInclude = true)]
|
||||
[NotMapped]
|
||||
[LoadableRelation(
|
||||
// language=PostgreSQL
|
||||
Projected = """
|
||||
(
|
||||
select
|
||||
count(*)::int
|
||||
@@ -143,9 +141,8 @@ namespace Kyoo.Abstractions.Models
|
||||
where
|
||||
e.season_id = id) as episodes_count
|
||||
"""
|
||||
)]
|
||||
public int EpisodesCount { get; set; }
|
||||
)]
|
||||
public int EpisodesCount { get; set; }
|
||||
|
||||
private int _EpisodesCount => Episodes!.Count;
|
||||
}
|
||||
private int _EpisodesCount => Episodes!.Count;
|
||||
}
|
||||
|
||||
@@ -21,155 +21,149 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using EntityFrameworkCore.Projectables;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A series or a movie.
|
||||
/// </summary>
|
||||
public class Show
|
||||
: IQuery,
|
||||
IResource,
|
||||
IMetadata,
|
||||
IOnMerge,
|
||||
IThumbnails,
|
||||
IAddedDate,
|
||||
ILibraryItem,
|
||||
IWatchlist
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<Show>.By(x => x.Name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A series or a movie.
|
||||
/// The title of this show.
|
||||
/// </summary>
|
||||
public class Show
|
||||
: IQuery,
|
||||
IResource,
|
||||
IMetadata,
|
||||
IOnMerge,
|
||||
IThumbnails,
|
||||
IAddedDate,
|
||||
ILibraryItem,
|
||||
IWatchlist
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<Show>.By(x => x.Name);
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
/// <summary>
|
||||
/// A catchphrase for this show.
|
||||
/// </summary>
|
||||
public string? Tagline { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; set; }
|
||||
/// <summary>
|
||||
/// The list of alternative titles of this show.
|
||||
/// </summary>
|
||||
public List<string> Aliases { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The title of this show.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// The summary of this show.
|
||||
/// </summary>
|
||||
public string? Overview { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A catchphrase for this show.
|
||||
/// </summary>
|
||||
public string? Tagline { get; set; }
|
||||
/// <summary>
|
||||
/// A list of tags that match this movie.
|
||||
/// </summary>
|
||||
public List<string> Tags { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The list of alternative titles of this show.
|
||||
/// </summary>
|
||||
public List<string> Aliases { get; set; } = new();
|
||||
/// <summary>
|
||||
/// The list of genres (themes) this show has.
|
||||
/// </summary>
|
||||
public List<Genre> Genres { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The summary of this show.
|
||||
/// </summary>
|
||||
public string? Overview { get; set; }
|
||||
/// <summary>
|
||||
/// Is this show airing, not aired yet or finished?
|
||||
/// </summary>
|
||||
public Status Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of tags that match this movie.
|
||||
/// </summary>
|
||||
public List<string> Tags { get; set; } = new();
|
||||
/// <summary>
|
||||
/// How well this item is rated? (from 0 to 100).
|
||||
/// </summary>
|
||||
public int Rating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of genres (themes) this show has.
|
||||
/// </summary>
|
||||
public List<Genre> Genres { get; set; } = new();
|
||||
/// <summary>
|
||||
/// The date this show started airing. It can be null if this is unknown.
|
||||
/// </summary>
|
||||
public DateOnly? StartAir { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is this show airing, not aired yet or finished?
|
||||
/// </summary>
|
||||
public Status Status { get; set; }
|
||||
/// <summary>
|
||||
/// The date this show finished airing.
|
||||
/// It can also be null if this is unknown.
|
||||
/// </summary>
|
||||
public DateOnly? EndAir { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How well this item is rated? (from 0 to 100).
|
||||
/// </summary>
|
||||
public int Rating { get; set; }
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date this show started airing. It can be null if this is unknown.
|
||||
/// </summary>
|
||||
public DateTime? StartAir { get; set; }
|
||||
/// <inheritdoc />
|
||||
public Image? Poster { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date this show finished airing.
|
||||
/// It can also be null if this is unknown.
|
||||
/// </summary>
|
||||
public DateTime? EndAir { get; set; }
|
||||
/// <inheritdoc />
|
||||
public Image? Thumbnail { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
/// <inheritdoc />
|
||||
public Image? Logo { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Poster { get; set; }
|
||||
/// <summary>
|
||||
/// A video of a few minutes that tease the content.
|
||||
/// </summary>
|
||||
public string? Trailer { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Thumbnail { get; set; }
|
||||
[JsonIgnore]
|
||||
[Column("start_air")]
|
||||
public DateOnly? AirDate => StartAir;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Image? Logo { get; set; }
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// A video of a few minutes that tease the content.
|
||||
/// </summary>
|
||||
public string? Trailer { get; set; }
|
||||
/// <summary>
|
||||
/// The ID of the Studio that made this show.
|
||||
/// </summary>
|
||||
public Guid? StudioId { get; set; }
|
||||
|
||||
[SerializeIgnore]
|
||||
[Column("start_air")]
|
||||
public DateTime? AirDate => StartAir;
|
||||
/// <summary>
|
||||
/// The Studio that made this show.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(StudioId))]
|
||||
public Studio? Studio { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
/// <summary>
|
||||
/// The different seasons in this show. If this is a movie, this list is always null or empty.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ICollection<Season>? Seasons { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the Studio that made this show.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Guid? StudioId { get; set; }
|
||||
/// <summary>
|
||||
/// The list of episodes in this show.
|
||||
/// If this is a movie, there will be a unique episode (with the seasonNumber and episodeNumber set to null).
|
||||
/// Having an episode is necessary to store metadata and tracks.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ICollection<Episode>? Episodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Studio that made this show.
|
||||
/// </summary>
|
||||
[LoadableRelation(nameof(StudioId))]
|
||||
public Studio? Studio { get; set; }
|
||||
/// <summary>
|
||||
/// The list of collections that contains this show.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ICollection<Collection>? Collections { get; set; }
|
||||
|
||||
// /// <summary>
|
||||
// /// The list of people that made this show.
|
||||
// /// </summary>
|
||||
// [SerializeIgnore] public ICollection<PeopleRole>? People { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The different seasons in this show. If this is a movie, this list is always null or empty.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<Season>? Seasons { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of episodes in this show.
|
||||
/// If this is a movie, there will be a unique episode (with the seasonNumber and episodeNumber set to null).
|
||||
/// Having an episode is necessary to store metadata and tracks.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<Episode>? Episodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of collections that contains this show.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<Collection>? Collections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The first episode of this show.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_FirstEpisode), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
// language=PostgreSQL
|
||||
Sql = """
|
||||
/// <summary>
|
||||
/// The first episode of this show.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_FirstEpisode), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
// language=PostgreSQL
|
||||
Sql = """
|
||||
select
|
||||
fe.* -- Episode as fe
|
||||
from (
|
||||
@@ -181,25 +175,25 @@ namespace Kyoo.Abstractions.Models
|
||||
where
|
||||
fe.number <= 1
|
||||
""",
|
||||
On = "show_id = \"this\".id"
|
||||
)]
|
||||
public Episode? FirstEpisode { get; set; }
|
||||
On = "show_id = \"this\".id"
|
||||
)]
|
||||
public Episode? FirstEpisode { get; set; }
|
||||
|
||||
private Episode? _FirstEpisode =>
|
||||
Episodes!
|
||||
.OrderBy(x => x.AbsoluteNumber)
|
||||
.ThenBy(x => x.SeasonNumber)
|
||||
.ThenBy(x => x.EpisodeNumber)
|
||||
.FirstOrDefault();
|
||||
private Episode? _FirstEpisode =>
|
||||
Episodes!
|
||||
.OrderBy(x => x.AbsoluteNumber)
|
||||
.ThenBy(x => x.SeasonNumber)
|
||||
.ThenBy(x => x.EpisodeNumber)
|
||||
.FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// The number of episodes in this show.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_EpisodesCount), OnlyOnInclude = true)]
|
||||
[NotMapped]
|
||||
[LoadableRelation(
|
||||
// language=PostgreSQL
|
||||
Projected = """
|
||||
/// <summary>
|
||||
/// The number of episodes in this show.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_EpisodesCount), OnlyOnInclude = true)]
|
||||
[NotMapped]
|
||||
[LoadableRelation(
|
||||
// language=PostgreSQL
|
||||
Projected = """
|
||||
(
|
||||
select
|
||||
count(*)::int
|
||||
@@ -208,84 +202,78 @@ namespace Kyoo.Abstractions.Models
|
||||
where
|
||||
e.show_id = "this".id) as episodes_count
|
||||
"""
|
||||
)]
|
||||
public int EpisodesCount { get; set; }
|
||||
)]
|
||||
public int EpisodesCount { get; set; }
|
||||
|
||||
private int _EpisodesCount => Episodes!.Count;
|
||||
private int _EpisodesCount => Episodes!.Count;
|
||||
|
||||
[SerializeIgnore]
|
||||
public ICollection<ShowWatchStatus>? Watched { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata of what an user as started/planned to watch.
|
||||
/// </summary>
|
||||
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
Sql = "show_watch_status",
|
||||
On = "show_id = \"this\".id and \"relation\".user_id = [current_user]"
|
||||
)]
|
||||
public ShowWatchStatus? WatchStatus { get; set; }
|
||||
|
||||
// There is a global query filter to filter by user so we just need to do single.
|
||||
private ShowWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnMerge(object merged)
|
||||
{
|
||||
// if (People != null)
|
||||
// {
|
||||
// foreach (PeopleRole link in People)
|
||||
// link.Show = this;
|
||||
// }
|
||||
if (Seasons != null)
|
||||
{
|
||||
foreach (Season season in Seasons)
|
||||
season.Show = this;
|
||||
}
|
||||
|
||||
if (Episodes != null)
|
||||
{
|
||||
foreach (Episode episode in Episodes)
|
||||
episode.Show = this;
|
||||
}
|
||||
}
|
||||
|
||||
public Show() { }
|
||||
|
||||
[JsonConstructor]
|
||||
public Show(string name)
|
||||
{
|
||||
if (name != null)
|
||||
{
|
||||
Slug = Utility.ToSlug(name);
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
[JsonIgnore]
|
||||
public ICollection<ShowWatchStatus>? Watched { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The enum containing show's status.
|
||||
/// Metadata of what an user as started/planned to watch.
|
||||
/// </summary>
|
||||
public enum Status
|
||||
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
|
||||
[LoadableRelation(
|
||||
Sql = "show_watch_status",
|
||||
On = "show_id = \"this\".id and \"relation\".user_id = [current_user]"
|
||||
)]
|
||||
public ShowWatchStatus? WatchStatus { get; set; }
|
||||
|
||||
// There is a global query filter to filter by user so we just need to do single.
|
||||
private ShowWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void OnMerge(object merged)
|
||||
{
|
||||
/// <summary>
|
||||
/// The status of the show is not known.
|
||||
/// </summary>
|
||||
Unknown,
|
||||
if (Seasons != null)
|
||||
{
|
||||
foreach (Season season in Seasons)
|
||||
season.Show = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The show has finished airing.
|
||||
/// </summary>
|
||||
Finished,
|
||||
if (Episodes != null)
|
||||
{
|
||||
foreach (Episode episode in Episodes)
|
||||
episode.Show = this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The show is still actively airing.
|
||||
/// </summary>
|
||||
Airing,
|
||||
public Show() { }
|
||||
|
||||
/// <summary>
|
||||
/// This show has not aired yet but has been announced.
|
||||
/// </summary>
|
||||
Planned
|
||||
[JsonConstructor]
|
||||
public Show(string name)
|
||||
{
|
||||
if (name != null)
|
||||
{
|
||||
Slug = Utility.ToSlug(name);
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The enum containing show's status.
|
||||
/// </summary>
|
||||
public enum Status
|
||||
{
|
||||
/// <summary>
|
||||
/// The status of the show is not known.
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// The show has finished airing.
|
||||
/// </summary>
|
||||
Finished,
|
||||
|
||||
/// <summary>
|
||||
/// The show is still actively airing.
|
||||
/// </summary>
|
||||
Airing,
|
||||
|
||||
/// <summary>
|
||||
/// This show has not aired yet but has been announced.
|
||||
/// </summary>
|
||||
Planned
|
||||
}
|
||||
|
||||
@@ -19,64 +19,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A studio that make shows.
|
||||
/// </summary>
|
||||
public class Studio : IQuery, IResource, IMetadata
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<Studio>.By(x => x.Name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A studio that make shows.
|
||||
/// The name of this studio.
|
||||
/// </summary>
|
||||
public class Studio : IQuery, IResource, IMetadata
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of shows that are made by this studio.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ICollection<Show>? Shows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of movies that are made by this studio.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ICollection<Movie>? Movies { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, empty, <see cref="Studio"/>.
|
||||
/// </summary>
|
||||
public Studio() { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Studio"/> with a specific name, the slug is calculated automatically.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the studio.</param>
|
||||
[JsonConstructor]
|
||||
public Studio(string name)
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<Studio>.By(x => x.Name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of this studio.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of shows that are made by this studio.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<Show>? Shows { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of movies that are made by this studio.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public ICollection<Movie>? Movies { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, empty, <see cref="Studio"/>.
|
||||
/// </summary>
|
||||
public Studio() { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Studio"/> with a specific name, the slug is calculated automatically.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the studio.</param>
|
||||
[JsonConstructor]
|
||||
public Studio(string name)
|
||||
if (name != null)
|
||||
{
|
||||
if (name != null)
|
||||
{
|
||||
Slug = Utility.ToSlug(name);
|
||||
Name = name;
|
||||
}
|
||||
Slug = Utility.ToSlug(name);
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,71 +19,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A single user of the app.
|
||||
/// </summary>
|
||||
public class User : IQuery, IResource, IAddedDate
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<User>.By(x => x.Username);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A single user of the app.
|
||||
/// A username displayed to the user.
|
||||
/// </summary>
|
||||
public class User : IQuery, IResource, IAddedDate
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user email address.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user password (hashed, it can't be read like that). The hashing format is implementation defined.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Does the user can sign-in with a password or only via oidc?
|
||||
/// </summary>
|
||||
public bool HasPassword => Password != null;
|
||||
|
||||
/// <summary>
|
||||
/// The list of permissions of the user. The format of this is implementation dependent.
|
||||
/// </summary>
|
||||
public string[] Permissions { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User settings
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Settings { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// User accounts on other services.
|
||||
/// </summary>
|
||||
public Dictionary<string, ExternalToken> ExternalId { get; set; } = new();
|
||||
|
||||
public User() { }
|
||||
|
||||
[JsonConstructor]
|
||||
public User(string username)
|
||||
{
|
||||
public static Sort DefaultSort => new Sort<User>.By(x => x.Username);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[MaxLength(256)]
|
||||
public string Slug { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A username displayed to the user.
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user email address.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user password (hashed, it can't be read like that). The hashing format is implementation defined.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of permissions of the user. The format of this is implementation dependent.
|
||||
/// </summary>
|
||||
public string[] Permissions { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A logo is a small image representing the resource.
|
||||
/// </summary>
|
||||
public Image? Logo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User settings
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Settings { get; set; } = new();
|
||||
|
||||
public User() { }
|
||||
|
||||
[JsonConstructor]
|
||||
public User(string username)
|
||||
if (username != null)
|
||||
{
|
||||
if (username != null)
|
||||
{
|
||||
Slug = Utility.ToSlug(username);
|
||||
Username = username;
|
||||
}
|
||||
Slug = Utility.ToSlug(username);
|
||||
Username = username;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalToken
|
||||
{
|
||||
/// <summary>
|
||||
/// The id of this user on the external service.
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The username on the external service.
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The link to the user profile on this website. Null if it does not exist.
|
||||
/// </summary>
|
||||
public string? ProfileUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A jwt token used to interact with the service.
|
||||
/// Do not forget to refresh it when using it if necessary.
|
||||
/// </summary>
|
||||
public JwtToken Token { get; set; }
|
||||
}
|
||||
|
||||
@@ -17,223 +17,263 @@
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Has the user started watching, is it planned?
|
||||
/// </summary>
|
||||
public enum WatchStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The user has already watched this.
|
||||
/// </summary>
|
||||
Completed,
|
||||
|
||||
/// <summary>
|
||||
/// The user started watching this but has not finished.
|
||||
/// </summary>
|
||||
Watching,
|
||||
|
||||
/// <summary>
|
||||
/// The user does not plan to continue watching.
|
||||
/// </summary>
|
||||
Droped,
|
||||
|
||||
/// <summary>
|
||||
/// The user has not started watching this but plans to.
|
||||
/// </summary>
|
||||
Planned,
|
||||
|
||||
/// <summary>
|
||||
/// The watch status was deleted and can not be retrived again.
|
||||
/// </summary>
|
||||
Deleted,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata of what an user as started/planned to watch.
|
||||
/// </summary>
|
||||
[SqlFirstColumn(nameof(UserId))]
|
||||
public class MovieWatchStatus : IAddedDate
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the user that started watching this episode.
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user that started watching this episode.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the movie started.
|
||||
/// </summary>
|
||||
public Guid MovieId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Movie"/> started.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Movie Movie { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date at which this item was played.
|
||||
/// </summary>
|
||||
public DateTime? PlayedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Has the user started watching, is it planned?
|
||||
/// </summary>
|
||||
public WatchStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the movie (in seconds).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching.
|
||||
/// </remarks>
|
||||
public int? WatchedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the movie (in percentage between 0 and 100).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching.
|
||||
/// </remarks>
|
||||
public int? WatchedPercent { get; set; }
|
||||
}
|
||||
|
||||
[SqlFirstColumn(nameof(UserId))]
|
||||
public class EpisodeWatchStatus : IAddedDate
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the user that started watching this episode.
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user that started watching this episode.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the episode started.
|
||||
/// </summary>
|
||||
public Guid? EpisodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Episode"/> started.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Episode Episode { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date at which this item was played.
|
||||
/// </summary>
|
||||
public DateTime? PlayedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Has the user started watching, is it planned?
|
||||
/// </summary>
|
||||
public WatchStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the episode (in seconds).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching.
|
||||
/// </remarks>
|
||||
public int? WatchedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the episode (in percentage between 0 and 100).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching or if the next episode is not started.
|
||||
/// </remarks>
|
||||
public int? WatchedPercent { get; set; }
|
||||
}
|
||||
|
||||
[SqlFirstColumn(nameof(UserId))]
|
||||
public class ShowWatchStatus : IAddedDate
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the user that started watching this episode.
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user that started watching this episode.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the show started.
|
||||
/// </summary>
|
||||
public Guid ShowId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Show"/> started.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Show Show { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date at which this item was played.
|
||||
/// </summary>
|
||||
public DateTime? PlayedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Has the user started watching, is it planned?
|
||||
/// </summary>
|
||||
public WatchStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of episodes the user has not seen.
|
||||
/// </summary>
|
||||
public int UnseenEpisodesCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the episode started.
|
||||
/// </summary>
|
||||
public Guid? NextEpisodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The next <see cref="Episode"/> to watch.
|
||||
/// </summary>
|
||||
public Episode? NextEpisode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the episode (in seconds).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching or if the next episode is not started.
|
||||
/// </remarks>
|
||||
public int? WatchedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the episode (in percentage between 0 and 100).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching or if the next episode is not started.
|
||||
/// </remarks>
|
||||
public int? WatchedPercent { get; set; }
|
||||
}
|
||||
|
||||
public class WatchStatus<T> : IAddedDate
|
||||
{
|
||||
/// <summary>
|
||||
/// Has the user started watching, is it planned?
|
||||
/// </summary>
|
||||
public enum WatchStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The user has already watched this.
|
||||
/// </summary>
|
||||
Completed,
|
||||
public required WatchStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user started watching this but has not finished.
|
||||
/// </summary>
|
||||
Watching,
|
||||
|
||||
/// <summary>
|
||||
/// The user does not plan to continue watching.
|
||||
/// </summary>
|
||||
Droped,
|
||||
|
||||
/// <summary>
|
||||
/// The user has not started watching this but plans to.
|
||||
/// </summary>
|
||||
Planned,
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata of what an user as started/planned to watch.
|
||||
/// The date at which this item was played.
|
||||
/// </summary>
|
||||
[SqlFirstColumn(nameof(UserId))]
|
||||
public class MovieWatchStatus : IAddedDate
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the user that started watching this episode.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Guid UserId { get; set; }
|
||||
public DateTime? PlayedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user that started watching this episode.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public User User { get; set; }
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the episode (in seconds).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching or if the next episode is not started.
|
||||
/// </remarks>
|
||||
public int? WatchedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the movie started.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Guid MovieId { get; set; }
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the episode (in percentage between 0 and 100).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching or if the next episode is not started.
|
||||
/// </remarks>
|
||||
public int? WatchedPercent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Movie"/> started.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Movie Movie { get; set; }
|
||||
/// <summary>
|
||||
/// The user that started watching this episode.
|
||||
/// </summary>
|
||||
public required User User { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date at which this item was played.
|
||||
/// </summary>
|
||||
public DateTime? PlayedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Has the user started watching, is it planned?
|
||||
/// </summary>
|
||||
public WatchStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the movie (in seconds).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching.
|
||||
/// </remarks>
|
||||
public int? WatchedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the movie (in percentage between 0 and 100).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching.
|
||||
/// </remarks>
|
||||
public int? WatchedPercent { get; set; }
|
||||
}
|
||||
|
||||
[SqlFirstColumn(nameof(UserId))]
|
||||
public class EpisodeWatchStatus : IAddedDate
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the user that started watching this episode.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user that started watching this episode.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the episode started.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Guid? EpisodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Episode"/> started.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Episode Episode { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date at which this item was played.
|
||||
/// </summary>
|
||||
public DateTime? PlayedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Has the user started watching, is it planned?
|
||||
/// </summary>
|
||||
public WatchStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the episode (in seconds).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching.
|
||||
/// </remarks>
|
||||
public int? WatchedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the episode (in percentage between 0 and 100).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching or if the next episode is not started.
|
||||
/// </remarks>
|
||||
public int? WatchedPercent { get; set; }
|
||||
}
|
||||
|
||||
[SqlFirstColumn(nameof(UserId))]
|
||||
public class ShowWatchStatus : IAddedDate
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the user that started watching this episode.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user that started watching this episode.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the show started.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Guid ShowId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Show"/> started.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Show Show { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DateTime AddedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date at which this item was played.
|
||||
/// </summary>
|
||||
public DateTime? PlayedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Has the user started watching, is it planned?
|
||||
/// </summary>
|
||||
public WatchStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of episodes the user has not seen.
|
||||
/// </summary>
|
||||
public int UnseenEpisodesCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the episode started.
|
||||
/// </summary>
|
||||
[SerializeIgnore]
|
||||
public Guid? NextEpisodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The next <see cref="Episode"/> to watch.
|
||||
/// </summary>
|
||||
public Episode? NextEpisode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the episode (in seconds).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching or if the next episode is not started.
|
||||
/// </remarks>
|
||||
public int? WatchedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the player has stopped watching the episode (in percentage between 0 and 100).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if the status is not Watching or if the next episode is not started.
|
||||
/// </remarks>
|
||||
public int? WatchedPercent { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// The episode/show/movie whose status changed
|
||||
/// </summary>
|
||||
public required T Resource { get; set; }
|
||||
}
|
||||
|
||||
@@ -18,37 +18,36 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Results of a search request.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The search item's type.</typeparam>
|
||||
public class SearchPage<T> : Page<T>
|
||||
where T : IResource
|
||||
{
|
||||
/// <summary>
|
||||
/// Results of a search request.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The search item's type.</typeparam>
|
||||
public class SearchPage<T> : Page<T>
|
||||
where T : IResource
|
||||
public SearchPage(
|
||||
SearchResult result,
|
||||
string @this,
|
||||
string? previous,
|
||||
string? next,
|
||||
string first
|
||||
)
|
||||
: base(result.Items, @this, previous, next, first)
|
||||
{
|
||||
public SearchPage(
|
||||
SearchResult result,
|
||||
string @this,
|
||||
string? previous,
|
||||
string? next,
|
||||
string first
|
||||
)
|
||||
: base(result.Items, @this, previous, next, first)
|
||||
{
|
||||
Query = result.Query;
|
||||
}
|
||||
Query = result.Query;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The query of the search request.
|
||||
/// </summary>
|
||||
public string? Query { get; init; }
|
||||
/// <summary>
|
||||
/// The query of the search request.
|
||||
/// </summary>
|
||||
public string? Query { get; init; }
|
||||
|
||||
public class SearchResult
|
||||
{
|
||||
public string? Query { get; set; }
|
||||
public class SearchResult
|
||||
{
|
||||
public string? Query { get; set; }
|
||||
|
||||
public ICollection<T> Items { get; set; }
|
||||
}
|
||||
public ICollection<T> Items { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,41 +16,40 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace Kyoo.Authentication.Models
|
||||
namespace Kyoo.Authentication.Models;
|
||||
|
||||
/// <summary>
|
||||
/// List of well known claims of kyoo
|
||||
/// </summary>
|
||||
public static class Claims
|
||||
{
|
||||
/// <summary>
|
||||
/// List of well known claims of kyoo
|
||||
/// The id of the user
|
||||
/// </summary>
|
||||
public static class Claims
|
||||
{
|
||||
/// <summary>
|
||||
/// The id of the user
|
||||
/// </summary>
|
||||
public static string Id => "id";
|
||||
public static string Id => "id";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the user
|
||||
/// </summary>
|
||||
public static string Name => "name";
|
||||
/// <summary>
|
||||
/// The name of the user
|
||||
/// </summary>
|
||||
public static string Name => "name";
|
||||
|
||||
/// <summary>
|
||||
/// The email of the user.
|
||||
/// </summary>
|
||||
public static string Email => "email";
|
||||
/// <summary>
|
||||
/// The email of the user.
|
||||
/// </summary>
|
||||
public static string Email => "email";
|
||||
|
||||
/// <summary>
|
||||
/// The list of permissions that the user has.
|
||||
/// </summary>
|
||||
public static string Permissions => "permissions";
|
||||
/// <summary>
|
||||
/// The list of permissions that the user has.
|
||||
/// </summary>
|
||||
public static string Permissions => "permissions";
|
||||
|
||||
/// <summary>
|
||||
/// The type of the token (either "access" or "refresh").
|
||||
/// </summary>
|
||||
public static string Type => "type";
|
||||
/// <summary>
|
||||
/// The type of the token (either "access" or "refresh").
|
||||
/// </summary>
|
||||
public static string Type => "type";
|
||||
|
||||
/// <summary>
|
||||
/// A guid used to identify a specific refresh token. This is only useful for the server to revokate tokens.
|
||||
/// </summary>
|
||||
public static string Guid => "guid";
|
||||
}
|
||||
/// <summary>
|
||||
/// A guid used to identify a specific refresh token. This is only useful for the server to revokate tokens.
|
||||
/// </summary>
|
||||
public static string Guid => "guid";
|
||||
}
|
||||
|
||||
@@ -18,43 +18,42 @@
|
||||
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Utils
|
||||
namespace Kyoo.Abstractions.Models.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// A class containing constant numbers.
|
||||
/// </summary>
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// A class containing constant numbers.
|
||||
/// A property to use on a Microsoft.AspNet.MVC.Route.Order property to mark it as an alternative route
|
||||
/// that won't be included on the swagger.
|
||||
/// </summary>
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// A property to use on a Microsoft.AspNet.MVC.Route.Order property to mark it as an alternative route
|
||||
/// that won't be included on the swagger.
|
||||
/// </summary>
|
||||
public const int AlternativeRoute = 1;
|
||||
public const int AlternativeRoute = 1;
|
||||
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by users.
|
||||
/// </summary>
|
||||
public const string UsersGroup = "0:Users";
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by users.
|
||||
/// </summary>
|
||||
public const string UsersGroup = "0:Users";
|
||||
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for main resources of kyoo.
|
||||
/// </summary>
|
||||
public const string ResourcesGroup = "1:Resources";
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for main resources of kyoo.
|
||||
/// </summary>
|
||||
public const string ResourcesGroup = "1:Resources";
|
||||
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>.
|
||||
/// It should be used for sub resources of kyoo that help define the main resources.
|
||||
/// </summary>
|
||||
public const string MetadataGroup = "2:Metadata";
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>.
|
||||
/// It should be used for sub resources of kyoo that help define the main resources.
|
||||
/// </summary>
|
||||
public const string MetadataGroup = "2:Metadata";
|
||||
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints useful for playback.
|
||||
/// </summary>
|
||||
public const string WatchGroup = "3:Watch";
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints useful for playback.
|
||||
/// </summary>
|
||||
public const string WatchGroup = "3:Watch";
|
||||
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by admins.
|
||||
/// </summary>
|
||||
public const string AdminGroup = "4:Admin";
|
||||
}
|
||||
/// <summary>
|
||||
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by admins.
|
||||
/// </summary>
|
||||
public const string AdminGroup = "4:Admin";
|
||||
}
|
||||
|
||||
@@ -196,8 +196,8 @@ public abstract record Filter<T> : Filter
|
||||
{
|
||||
return (
|
||||
from lq in Parse.Char('"').Or(Parse.Char('\''))
|
||||
from str in Parse.AnyChar.Where(x => x is not '"' and not '\'').Many().Text()
|
||||
from rq in Parse.Char('"').Or(Parse.Char('\''))
|
||||
from str in Parse.AnyChar.Where(x => x != lq).Many().Text()
|
||||
from rq in Parse.Char(lq)
|
||||
select str
|
||||
).Or(Parse.LetterOrDigit.Many().Text());
|
||||
}
|
||||
@@ -205,8 +205,7 @@ public abstract record Filter<T> : Filter
|
||||
if (type.IsEnum)
|
||||
{
|
||||
return Parse
|
||||
.LetterOrDigit
|
||||
.Many()
|
||||
.LetterOrDigit.Many()
|
||||
.Text()
|
||||
.Then(x =>
|
||||
{
|
||||
@@ -259,14 +258,11 @@ public abstract record Filter<T> : Filter
|
||||
}
|
||||
|
||||
PropertyInfo? propInfo = types
|
||||
.Select(
|
||||
x =>
|
||||
x.GetProperty(
|
||||
prop,
|
||||
BindingFlags.IgnoreCase
|
||||
| BindingFlags.Public
|
||||
| BindingFlags.Instance
|
||||
)
|
||||
.Select(x =>
|
||||
x.GetProperty(
|
||||
prop,
|
||||
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
|
||||
)
|
||||
)
|
||||
.FirstOrDefault();
|
||||
if (propInfo == null)
|
||||
|
||||
@@ -24,215 +24,222 @@ using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Utils
|
||||
namespace Kyoo.Abstractions.Models.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// A class that represent a resource. It is made to be used as a parameter in a query and not used somewhere else
|
||||
/// on the application.
|
||||
/// This class allow routes to be used via ether IDs or Slugs, this is suitable for every <see cref="IResource"/>.
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(IdentifierConvertor))]
|
||||
public class Identifier
|
||||
{
|
||||
/// <summary>
|
||||
/// A class that represent a resource. It is made to be used as a parameter in a query and not used somewhere else
|
||||
/// on the application.
|
||||
/// This class allow routes to be used via ether IDs or Slugs, this is suitable for every <see cref="IResource"/>.
|
||||
/// The ID of the resource or null if the slug is specified.
|
||||
/// </summary>
|
||||
[TypeConverter(typeof(IdentifierConvertor))]
|
||||
public class Identifier
|
||||
private readonly Guid? _id;
|
||||
|
||||
/// <summary>
|
||||
/// The slug of the resource or null if the id is specified.
|
||||
/// </summary>
|
||||
private readonly string? _slug;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Identifier"/> for the given id.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the resource.</param>
|
||||
public Identifier(Guid id)
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the resource or null if the slug is specified.
|
||||
/// </summary>
|
||||
private readonly Guid? _id;
|
||||
_id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The slug of the resource or null if the id is specified.
|
||||
/// </summary>
|
||||
private readonly string? _slug;
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Identifier"/> for the given slug.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the resource.</param>
|
||||
public Identifier(string slug)
|
||||
{
|
||||
_slug = slug;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Identifier"/> for the given id.
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the resource.</param>
|
||||
public Identifier(Guid id)
|
||||
/// <summary>
|
||||
/// Pattern match out of the identifier to a resource.
|
||||
/// </summary>
|
||||
/// <param name="idFunc">The function to match the ID to a type <typeparamref name="T"/>.</param>
|
||||
/// <param name="slugFunc">The function to match the slug to a type <typeparamref name="T"/>.</param>
|
||||
/// <typeparam name="T">The return type that will be converted to from an ID or a slug.</typeparam>
|
||||
/// <returns>
|
||||
/// The result of the <paramref name="idFunc"/> or <paramref name="slugFunc"/> depending on the pattern.
|
||||
/// </returns>
|
||||
/// <example>
|
||||
/// Example usage:
|
||||
/// <code lang="csharp">
|
||||
/// T ret = await identifier.Match(
|
||||
/// id => _repository.GetOrDefault(id),
|
||||
/// slug => _repository.GetOrDefault(slug)
|
||||
/// );
|
||||
/// </code>
|
||||
/// </example>
|
||||
public T Match<T>(Func<Guid, T> idFunc, Func<string, T> slugFunc)
|
||||
{
|
||||
return _id.HasValue ? idFunc(_id.Value) : slugFunc(_slug!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match a custom type to an identifier. This can be used for wrapped resources (see example for more details).
|
||||
/// </summary>
|
||||
/// <param name="idGetter">An expression to retrieve an ID from the type <typeparamref name="T"/>.</param>
|
||||
/// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param>
|
||||
/// <typeparam name="T">The type to match against this identifier.</typeparam>
|
||||
/// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns>
|
||||
/// <example>
|
||||
/// <code lang="csharp">
|
||||
/// identifier.Matcher<Season>(x => x.ShowID, x => x.Show.Slug)
|
||||
/// </code>
|
||||
/// </example>
|
||||
public Filter<T> Matcher<T>(
|
||||
Expression<Func<T, Guid>> idGetter,
|
||||
Expression<Func<T, string>> slugGetter
|
||||
)
|
||||
{
|
||||
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
|
||||
BinaryExpression equal = Expression.Equal(
|
||||
_id.HasValue ? idGetter.Body : slugGetter.Body,
|
||||
self
|
||||
);
|
||||
ICollection<ParameterExpression> parameters = _id.HasValue
|
||||
? idGetter.Parameters
|
||||
: slugGetter.Parameters;
|
||||
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
|
||||
return new Filter<T>.Lambda(lambda);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A matcher overload for nullable IDs. See
|
||||
/// <see cref="Matcher{T}(Expression{Func{T,Guid}},Expression{Func{T,string}})"/>
|
||||
/// for more details.
|
||||
/// </summary>
|
||||
/// <param name="idGetter">An expression to retrieve an ID from the type <typeparamref name="T"/>.</param>
|
||||
/// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param>
|
||||
/// <typeparam name="T">The type to match against this identifier.</typeparam>
|
||||
/// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns>
|
||||
public Filter<T> Matcher<T>(
|
||||
Expression<Func<T, Guid?>> idGetter,
|
||||
Expression<Func<T, string>> slugGetter
|
||||
)
|
||||
{
|
||||
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
|
||||
BinaryExpression equal = Expression.Equal(
|
||||
_id.HasValue ? idGetter.Body : slugGetter.Body,
|
||||
self
|
||||
);
|
||||
ICollection<ParameterExpression> parameters = _id.HasValue
|
||||
? idGetter.Parameters
|
||||
: slugGetter.Parameters;
|
||||
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
|
||||
return new Filter<T>.Lambda(lambda);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return true if this <see cref="Identifier"/> match a resource.
|
||||
/// </summary>
|
||||
/// <param name="resource">The resource to match</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the <paramref name="resource"/> match this identifier, <c>false</c> otherwise.
|
||||
/// </returns>
|
||||
public bool IsSame(IResource resource)
|
||||
{
|
||||
return Match(id => resource.Id == id, slug => resource.Slug == slug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a filter to get this <see cref="Identifier"/> match a given resource.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of resource to match against.</typeparam>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the given resource match this identifier, <c>false</c> otherwise.
|
||||
/// </returns>
|
||||
public Filter<T> IsSame<T>()
|
||||
where T : IResource
|
||||
{
|
||||
return _id.HasValue ? new Filter<T>.Eq("Id", _id.Value) : new Filter<T>.Eq("Slug", _slug!);
|
||||
}
|
||||
|
||||
public bool Is(Guid uid)
|
||||
{
|
||||
return _id.HasValue && _id.Value == uid;
|
||||
}
|
||||
|
||||
public bool Is(string slug)
|
||||
{
|
||||
return !_id.HasValue && _slug == slug;
|
||||
}
|
||||
|
||||
private Expression<Func<T, bool>> _IsSameExpression<T>()
|
||||
where T : IResource
|
||||
{
|
||||
return _id.HasValue ? x => x.Id == _id.Value : x => x.Slug == _slug;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return an expression that return true if this <see cref="Identifier"/> is containing in a collection.
|
||||
/// </summary>
|
||||
/// <param name="listGetter">An expression to retrieve the list to check.</param>
|
||||
/// <typeparam name="T">The type that contain the list to check.</typeparam>
|
||||
/// <typeparam name="T2">The type of resource to check this identifier against.</typeparam>
|
||||
/// <returns>An expression to check if this <see cref="Identifier"/> is contained.</returns>
|
||||
public Filter<T> IsContainedIn<T, T2>(Expression<Func<T, IEnumerable<T2>?>> listGetter)
|
||||
where T2 : IResource
|
||||
{
|
||||
MethodInfo method = typeof(Enumerable)
|
||||
.GetMethods()
|
||||
.Where(x => x.Name == nameof(Enumerable.Any))
|
||||
.FirstOrDefault(x => x.GetParameters().Length == 2)!
|
||||
.MakeGenericMethod(typeof(T2));
|
||||
MethodCallExpression call = Expression.Call(
|
||||
null,
|
||||
method,
|
||||
listGetter.Body,
|
||||
_IsSameExpression<T2>()
|
||||
);
|
||||
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(
|
||||
call,
|
||||
listGetter.Parameters
|
||||
);
|
||||
return new Filter<T>.Lambda(lambda);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return _id.HasValue ? _id.Value.ToString() : _slug!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom <see cref="TypeConverter"/> used to convert int or strings to an <see cref="Identifier"/>.
|
||||
/// </summary>
|
||||
public class IdentifierConvertor : TypeConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
_id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Identifier"/> for the given slug.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the resource.</param>
|
||||
public Identifier(string slug)
|
||||
{
|
||||
_slug = slug;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pattern match out of the identifier to a resource.
|
||||
/// </summary>
|
||||
/// <param name="idFunc">The function to match the ID to a type <typeparamref name="T"/>.</param>
|
||||
/// <param name="slugFunc">The function to match the slug to a type <typeparamref name="T"/>.</param>
|
||||
/// <typeparam name="T">The return type that will be converted to from an ID or a slug.</typeparam>
|
||||
/// <returns>
|
||||
/// The result of the <paramref name="idFunc"/> or <paramref name="slugFunc"/> depending on the pattern.
|
||||
/// </returns>
|
||||
/// <example>
|
||||
/// Example usage:
|
||||
/// <code lang="csharp">
|
||||
/// T ret = await identifier.Match(
|
||||
/// id => _repository.GetOrDefault(id),
|
||||
/// slug => _repository.GetOrDefault(slug)
|
||||
/// );
|
||||
/// </code>
|
||||
/// </example>
|
||||
public T Match<T>(Func<Guid, T> idFunc, Func<string, T> slugFunc)
|
||||
{
|
||||
return _id.HasValue ? idFunc(_id.Value) : slugFunc(_slug!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match a custom type to an identifier. This can be used for wrapped resources (see example for more details).
|
||||
/// </summary>
|
||||
/// <param name="idGetter">An expression to retrieve an ID from the type <typeparamref name="T"/>.</param>
|
||||
/// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param>
|
||||
/// <typeparam name="T">The type to match against this identifier.</typeparam>
|
||||
/// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns>
|
||||
/// <example>
|
||||
/// <code lang="csharp">
|
||||
/// identifier.Matcher<Season>(x => x.ShowID, x => x.Show.Slug)
|
||||
/// </code>
|
||||
/// </example>
|
||||
public Filter<T> Matcher<T>(
|
||||
Expression<Func<T, Guid>> idGetter,
|
||||
Expression<Func<T, string>> slugGetter
|
||||
)
|
||||
{
|
||||
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
|
||||
BinaryExpression equal = Expression.Equal(
|
||||
_id.HasValue ? idGetter.Body : slugGetter.Body,
|
||||
self
|
||||
);
|
||||
ICollection<ParameterExpression> parameters = _id.HasValue
|
||||
? idGetter.Parameters
|
||||
: slugGetter.Parameters;
|
||||
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
|
||||
return new Filter<T>.Lambda(lambda);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A matcher overload for nullable IDs. See
|
||||
/// <see cref="Matcher{T}(Expression{Func{T,Guid}},Expression{Func{T,string}})"/>
|
||||
/// for more details.
|
||||
/// </summary>
|
||||
/// <param name="idGetter">An expression to retrieve an ID from the type <typeparamref name="T"/>.</param>
|
||||
/// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param>
|
||||
/// <typeparam name="T">The type to match against this identifier.</typeparam>
|
||||
/// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns>
|
||||
public Filter<T> Matcher<T>(
|
||||
Expression<Func<T, Guid?>> idGetter,
|
||||
Expression<Func<T, string>> slugGetter
|
||||
)
|
||||
{
|
||||
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
|
||||
BinaryExpression equal = Expression.Equal(
|
||||
_id.HasValue ? idGetter.Body : slugGetter.Body,
|
||||
self
|
||||
);
|
||||
ICollection<ParameterExpression> parameters = _id.HasValue
|
||||
? idGetter.Parameters
|
||||
: slugGetter.Parameters;
|
||||
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
|
||||
return new Filter<T>.Lambda(lambda);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return true if this <see cref="Identifier"/> match a resource.
|
||||
/// </summary>
|
||||
/// <param name="resource">The resource to match</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the <paramref name="resource"/> match this identifier, <c>false</c> otherwise.
|
||||
/// </returns>
|
||||
public bool IsSame(IResource resource)
|
||||
{
|
||||
return Match(id => resource.Id == id, slug => resource.Slug == slug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a filter to get this <see cref="Identifier"/> match a given resource.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of resource to match against.</typeparam>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the given resource match this identifier, <c>false</c> otherwise.
|
||||
/// </returns>
|
||||
public Filter<T> IsSame<T>()
|
||||
where T : IResource
|
||||
{
|
||||
return _id.HasValue
|
||||
? new Filter<T>.Eq("Id", _id.Value)
|
||||
: new Filter<T>.Eq("Slug", _slug!);
|
||||
}
|
||||
|
||||
private Expression<Func<T, bool>> _IsSameExpression<T>()
|
||||
where T : IResource
|
||||
{
|
||||
return _id.HasValue ? x => x.Id == _id.Value : x => x.Slug == _slug;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return an expression that return true if this <see cref="Identifier"/> is containing in a collection.
|
||||
/// </summary>
|
||||
/// <param name="listGetter">An expression to retrieve the list to check.</param>
|
||||
/// <typeparam name="T">The type that contain the list to check.</typeparam>
|
||||
/// <typeparam name="T2">The type of resource to check this identifier against.</typeparam>
|
||||
/// <returns>An expression to check if this <see cref="Identifier"/> is contained.</returns>
|
||||
public Filter<T> IsContainedIn<T, T2>(Expression<Func<T, IEnumerable<T2>?>> listGetter)
|
||||
where T2 : IResource
|
||||
{
|
||||
MethodInfo method = typeof(Enumerable)
|
||||
.GetMethods()
|
||||
.Where(x => x.Name == nameof(Enumerable.Any))
|
||||
.FirstOrDefault(x => x.GetParameters().Length == 2)!
|
||||
.MakeGenericMethod(typeof(T2));
|
||||
MethodCallExpression call = Expression.Call(
|
||||
null,
|
||||
method,
|
||||
listGetter.Body,
|
||||
_IsSameExpression<T2>()
|
||||
);
|
||||
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(
|
||||
call,
|
||||
listGetter.Parameters
|
||||
);
|
||||
return new Filter<T>.Lambda(lambda);
|
||||
if (sourceType == typeof(Guid) || sourceType == typeof(string))
|
||||
return true;
|
||||
return base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
public override object ConvertFrom(
|
||||
ITypeDescriptorContext? context,
|
||||
CultureInfo? culture,
|
||||
object value
|
||||
)
|
||||
{
|
||||
return _id.HasValue ? _id.Value.ToString() : _slug!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom <see cref="TypeConverter"/> used to convert int or strings to an <see cref="Identifier"/>.
|
||||
/// </summary>
|
||||
public class IdentifierConvertor : TypeConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
if (sourceType == typeof(int) || sourceType == typeof(string))
|
||||
return true;
|
||||
return base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ConvertFrom(
|
||||
ITypeDescriptorContext? context,
|
||||
CultureInfo? culture,
|
||||
object value
|
||||
)
|
||||
{
|
||||
if (value is Guid id)
|
||||
return new Identifier(id);
|
||||
if (value is not string slug)
|
||||
return base.ConvertFrom(context, culture, value)!;
|
||||
return Guid.TryParse(slug, out id) ? new Identifier(id) : new Identifier(slug);
|
||||
}
|
||||
if (value is Guid id)
|
||||
return new Identifier(id);
|
||||
if (value is not string slug)
|
||||
return base.ConvertFrom(context, culture, value)!;
|
||||
return Guid.TryParse(slug, out id) ? new Identifier(id) : new Identifier(slug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,17 +62,14 @@ public class Include<T> : Include
|
||||
.SelectMany(key =>
|
||||
{
|
||||
var relations = types
|
||||
.Select(
|
||||
x =>
|
||||
x.GetProperty(
|
||||
key,
|
||||
BindingFlags.IgnoreCase
|
||||
| BindingFlags.Public
|
||||
| BindingFlags.Instance
|
||||
)!
|
||||
.Select(x =>
|
||||
x.GetProperty(
|
||||
key,
|
||||
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
|
||||
)!
|
||||
)
|
||||
.Select(
|
||||
prop => (prop, attr: prop?.GetCustomAttribute<LoadableRelationAttribute>()!)
|
||||
.Select(prop =>
|
||||
(prop, attr: prop?.GetCustomAttribute<LoadableRelationAttribute>()!)
|
||||
)
|
||||
.Where(x => x.prop != null && x.attr != null)
|
||||
.ToList();
|
||||
|
||||
@@ -18,56 +18,55 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Information about the pagination. How many items should be displayed and where to start.
|
||||
/// </summary>
|
||||
public class Pagination
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about the pagination. How many items should be displayed and where to start.
|
||||
/// The count of items to return.
|
||||
/// </summary>
|
||||
public class Pagination
|
||||
public int Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where to start? Using the given sort.
|
||||
/// </summary>
|
||||
public Guid? AfterID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should the previous page be returned instead of the next?
|
||||
/// </summary>
|
||||
public bool Reverse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Pagination"/> with default values.
|
||||
/// </summary>
|
||||
public Pagination()
|
||||
{
|
||||
/// <summary>
|
||||
/// The count of items to return.
|
||||
/// </summary>
|
||||
public int Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where to start? Using the given sort.
|
||||
/// </summary>
|
||||
public Guid? AfterID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should the previous page be returned instead of the next?
|
||||
/// </summary>
|
||||
public bool Reverse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Pagination"/> with default values.
|
||||
/// </summary>
|
||||
public Pagination()
|
||||
{
|
||||
Limit = 50;
|
||||
AfterID = null;
|
||||
Reverse = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Pagination"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="count">Set the <see cref="Limit"/> value</param>
|
||||
/// <param name="afterID">Set the <see cref="AfterID"/> value. If not specified, it will start from the start</param>
|
||||
/// <param name="reverse">Should the previous page be returned instead of the next?</param>
|
||||
public Pagination(int count, Guid? afterID = null, bool reverse = false)
|
||||
{
|
||||
Limit = count;
|
||||
AfterID = afterID;
|
||||
Reverse = reverse;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly create a new pagination from a limit number.
|
||||
/// </summary>
|
||||
/// <param name="limit">Set the <see cref="Limit"/> value</param>
|
||||
/// <returns>A new <see cref="Pagination"/> instance</returns>
|
||||
public static implicit operator Pagination(int limit) => new(limit);
|
||||
Limit = 50;
|
||||
AfterID = null;
|
||||
Reverse = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Pagination"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="count">Set the <see cref="Limit"/> value</param>
|
||||
/// <param name="afterID">Set the <see cref="AfterID"/> value. If not specified, it will start from the start</param>
|
||||
/// <param name="reverse">Should the previous page be returned instead of the next?</param>
|
||||
public Pagination(int count, Guid? afterID = null, bool reverse = false)
|
||||
{
|
||||
Limit = count;
|
||||
AfterID = afterID;
|
||||
Reverse = reverse;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly create a new pagination from a limit number.
|
||||
/// </summary>
|
||||
/// <param name="limit">Set the <see cref="Limit"/> value</param>
|
||||
/// <returns>A new <see cref="Pagination"/> instance</returns>
|
||||
public static implicit operator Pagination(int limit) => new(limit);
|
||||
}
|
||||
|
||||
@@ -18,44 +18,39 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Kyoo.Abstractions.Models.Utils
|
||||
namespace Kyoo.Abstractions.Models.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// The list of errors that where made in the request.
|
||||
/// </summary>
|
||||
public class RequestError
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of errors that where made in the request.
|
||||
/// </summary>
|
||||
public class RequestError
|
||||
/// <example><c>["InvalidFilter: no field 'startYear' on a collection"]</c></example>
|
||||
public string[] Errors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="RequestError"/> with one error.
|
||||
/// </summary>
|
||||
/// <param name="error">The error to specify in the response.</param>
|
||||
public RequestError(string error)
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of errors that where made in the request.
|
||||
/// </summary>
|
||||
/// <example><c>["InvalidFilter: no field 'startYear' on a collection"]</c></example>
|
||||
public string[] Errors { get; set; }
|
||||
if (error == null)
|
||||
throw new ArgumentNullException(nameof(error));
|
||||
Errors = new[] { error };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="RequestError"/> with one error.
|
||||
/// </summary>
|
||||
/// <param name="error">The error to specify in the response.</param>
|
||||
public RequestError(string error)
|
||||
{
|
||||
if (error == null)
|
||||
throw new ArgumentNullException(nameof(error));
|
||||
Errors = new[] { error };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="RequestError"/> with multiple errors.
|
||||
/// </summary>
|
||||
/// <param name="errors">The errors to specify in the response.</param>
|
||||
public RequestError(string[] errors)
|
||||
{
|
||||
if (errors == null || !errors.Any())
|
||||
throw new ArgumentException(
|
||||
"Errors must be non null and not empty",
|
||||
nameof(errors)
|
||||
);
|
||||
Errors = errors;
|
||||
}
|
||||
/// <summary>
|
||||
/// Create a new <see cref="RequestError"/> with multiple errors.
|
||||
/// </summary>
|
||||
/// <param name="errors">The errors to specify in the response.</param>
|
||||
public RequestError(string[] errors)
|
||||
{
|
||||
if (errors == null || !errors.Any())
|
||||
throw new ArgumentException("Errors must be non null and not empty", nameof(errors));
|
||||
Errors = errors;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,21 +16,20 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Information about the pagination. How many items should be displayed and where to start.
|
||||
/// </summary>
|
||||
public class SearchPagination
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about the pagination. How many items should be displayed and where to start.
|
||||
/// The count of items to return.
|
||||
/// </summary>
|
||||
public class SearchPagination
|
||||
{
|
||||
/// <summary>
|
||||
/// The count of items to return.
|
||||
/// </summary>
|
||||
public int Limit { get; set; } = 50;
|
||||
public int Limit { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Where to start? How many items to skip?
|
||||
/// </summary>
|
||||
public int? Skip { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Where to start? How many items to skip?
|
||||
/// </summary>
|
||||
public int? Skip { get; set; }
|
||||
}
|
||||
|
||||
@@ -25,112 +25,113 @@ using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Utils;
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers
|
||||
{
|
||||
public record Sort;
|
||||
namespace Kyoo.Abstractions.Controllers;
|
||||
|
||||
public record Sort;
|
||||
|
||||
/// <summary>
|
||||
/// Information about how a query should be sorted. What factor should decide the sort and in which order.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">For witch type this sort applies</typeparam>
|
||||
public record Sort<T> : Sort
|
||||
where T : IQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about how a query should be sorted. What factor should decide the sort and in which order.
|
||||
/// Sort by a specific key
|
||||
/// </summary>
|
||||
/// <typeparam name="T">For witch type this sort applies</typeparam>
|
||||
public record Sort<T> : Sort
|
||||
where T : IQuery
|
||||
/// <param name="Key">The sort keys. This members will be used to sort the results.</param>
|
||||
/// <param name="Desendant">
|
||||
/// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order.
|
||||
/// </param>
|
||||
public record By(string Key, bool Desendant = false) : Sort<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Sort by a specific key
|
||||
/// </summary>
|
||||
/// <param name="Key">The sort keys. This members will be used to sort the results.</param>
|
||||
/// <param name="Desendant">
|
||||
/// <param name="key">The sort keys. This members will be used to sort the results.</param>
|
||||
/// <param name="desendant">
|
||||
/// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order.
|
||||
/// </param>
|
||||
public record By(string Key, bool Desendant = false) : Sort<T>
|
||||
public By(Expression<Func<T, object?>> key, bool desendant = false)
|
||||
: this(Utility.GetPropertyName(key), desendant) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sort by multiple keys.
|
||||
/// </summary>
|
||||
/// <param name="List">The list of keys to sort by.</param>
|
||||
public record Conglomerate(params Sort<T>[] List) : Sort<T>;
|
||||
|
||||
/// <summary>Sort randomly items</summary>
|
||||
public record Random(uint Seed) : Sort<T>
|
||||
{
|
||||
public Random()
|
||||
: this(0)
|
||||
{
|
||||
/// <summary>
|
||||
/// Sort by a specific key
|
||||
/// </summary>
|
||||
/// <param name="key">The sort keys. This members will be used to sort the results.</param>
|
||||
/// <param name="desendant">
|
||||
/// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order.
|
||||
/// </param>
|
||||
public By(Expression<Func<T, object?>> key, bool desendant = false)
|
||||
: this(Utility.GetPropertyName(key), desendant) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sort by multiple keys.
|
||||
/// </summary>
|
||||
/// <param name="List">The list of keys to sort by.</param>
|
||||
public record Conglomerate(params Sort<T>[] List) : Sort<T>;
|
||||
|
||||
/// <summary>Sort randomly items</summary>
|
||||
public record Random(uint Seed) : Sort<T>
|
||||
{
|
||||
public Random()
|
||||
: this(0)
|
||||
{
|
||||
uint seed = BitConverter.ToUInt32(
|
||||
BitConverter.GetBytes(new System.Random().Next(int.MinValue, int.MaxValue)),
|
||||
0
|
||||
);
|
||||
Seed = seed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The default sort method for the given type.</summary>
|
||||
public record Default : Sort<T>
|
||||
{
|
||||
public void Deconstruct(out Sort<T> value)
|
||||
{
|
||||
value = (Sort<T>)T.DefaultSort;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Sort{T}"/> instance from a key's name (case insensitive).
|
||||
/// </summary>
|
||||
/// <param name="sortBy">A key name with an optional order specifier. Format: "key:asc", "key:desc" or "key".</param>
|
||||
/// <param name="seed">The random seed.</param>
|
||||
/// <exception cref="ArgumentException">An invalid key or sort specifier as been given.</exception>
|
||||
/// <returns>A <see cref="Sort{T}"/> for the given string</returns>
|
||||
public static Sort<T> From(string? sortBy, uint seed)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sortBy) || sortBy == "default")
|
||||
return new Default();
|
||||
if (sortBy == "random")
|
||||
return new Random(seed);
|
||||
if (sortBy.Contains(','))
|
||||
return new Conglomerate(sortBy.Split(',').Select(x => From(x, seed)).ToArray());
|
||||
|
||||
if (sortBy.StartsWith("random:"))
|
||||
return new Random(uint.Parse(sortBy["random:".Length..]));
|
||||
|
||||
string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy;
|
||||
string? order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null;
|
||||
bool desendant = order switch
|
||||
{
|
||||
"desc" => true,
|
||||
"asc" => false,
|
||||
null => false,
|
||||
_
|
||||
=> throw new ValidationException(
|
||||
$"The sort order, if set, should be :asc or :desc but it was :{order}."
|
||||
)
|
||||
};
|
||||
|
||||
Type[] types =
|
||||
typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
|
||||
PropertyInfo? property = types
|
||||
.Select(
|
||||
x =>
|
||||
x.GetProperty(
|
||||
key,
|
||||
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
|
||||
)
|
||||
)
|
||||
.FirstOrDefault(x => x != null);
|
||||
if (property == null)
|
||||
throw new ValidationException("The given sort key is not valid.");
|
||||
return new By(property.Name, desendant);
|
||||
uint seed = BitConverter.ToUInt32(
|
||||
BitConverter.GetBytes(new System.Random().Next(int.MinValue, int.MaxValue)),
|
||||
0
|
||||
);
|
||||
Seed = seed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The default sort method for the given type.</summary>
|
||||
public record Default : Sort<T>
|
||||
{
|
||||
public void Deconstruct(out Sort<T> value)
|
||||
{
|
||||
value = (Sort<T>)T.DefaultSort;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Sort{T}"/> instance from a key's name (case insensitive).
|
||||
/// </summary>
|
||||
/// <param name="sortBy">A key name with an optional order specifier. Format: "key:asc", "key:desc" or "key".</param>
|
||||
/// <param name="seed">The random seed.</param>
|
||||
/// <exception cref="ArgumentException">An invalid key or sort specifier as been given.</exception>
|
||||
/// <returns>A <see cref="Sort{T}"/> for the given string</returns>
|
||||
public static Sort<T> From(string? sortBy, uint seed)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sortBy) || sortBy == "default")
|
||||
return new Default();
|
||||
if (sortBy == "random")
|
||||
return new Random(seed);
|
||||
if (sortBy.Contains(','))
|
||||
return new Conglomerate(sortBy.Split(',').Select(x => From(x, seed)).ToArray());
|
||||
|
||||
if (sortBy.StartsWith("random:"))
|
||||
{
|
||||
if (uint.TryParse(sortBy["random:".Length..], out uint sseed))
|
||||
return new Random(sseed);
|
||||
throw new ValidationException("Invalid random seed specified. Expected a number.");
|
||||
}
|
||||
|
||||
string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy;
|
||||
string? order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null;
|
||||
bool desendant = order switch
|
||||
{
|
||||
"desc" => true,
|
||||
"asc" => false,
|
||||
null => false,
|
||||
_
|
||||
=> throw new ValidationException(
|
||||
$"The sort order, if set, should be :asc or :desc but it was :{order}."
|
||||
)
|
||||
};
|
||||
|
||||
Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
|
||||
PropertyInfo? property = types
|
||||
.Select(x =>
|
||||
x.GetProperty(
|
||||
key,
|
||||
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
|
||||
)
|
||||
)
|
||||
.FirstOrDefault(x => x != null);
|
||||
if (property == null)
|
||||
throw new ValidationException("The given sort key is not valid.");
|
||||
return new By(property.Name, desendant);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,21 +16,20 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace Kyoo.Abstractions.Models
|
||||
namespace Kyoo.Abstractions.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The links to see a movie or an episode.
|
||||
/// </summary>
|
||||
public class VideoLinks
|
||||
{
|
||||
/// <summary>
|
||||
/// The links to see a movie or an episode.
|
||||
/// The direct link to the unprocessed video (pristine quality).
|
||||
/// </summary>
|
||||
public class VideoLinks
|
||||
{
|
||||
/// <summary>
|
||||
/// The direct link to the unprocessed video (pristine quality).
|
||||
/// </summary>
|
||||
public string Direct { get; set; }
|
||||
public string Direct { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The link to an HLS master playlist containing all qualities available for this video.
|
||||
/// </summary>
|
||||
public string Hls { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// The link to an HLS master playlist containing all qualities available for this video.
|
||||
/// </summary>
|
||||
public string Hls { get; set; }
|
||||
}
|
||||
|
||||
@@ -21,56 +21,55 @@ using Autofac.Builder;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Utils;
|
||||
|
||||
namespace Kyoo.Abstractions
|
||||
namespace Kyoo.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// A static class with helper functions to setup external modules
|
||||
/// </summary>
|
||||
public static class Module
|
||||
{
|
||||
/// <summary>
|
||||
/// A static class with helper functions to setup external modules
|
||||
/// Register a new repository to the container.
|
||||
/// </summary>
|
||||
public static class Module
|
||||
/// <param name="builder">The container</param>
|
||||
/// <typeparam name="T">The type of the repository.</typeparam>
|
||||
/// <remarks>
|
||||
/// If your repository implements a special interface, please use <see cref="RegisterRepository{T,T2}"/>
|
||||
/// </remarks>
|
||||
/// <returns>The initial container.</returns>
|
||||
public static IRegistrationBuilder<
|
||||
T,
|
||||
ConcreteReflectionActivatorData,
|
||||
SingleRegistrationStyle
|
||||
> RegisterRepository<T>(this ContainerBuilder builder)
|
||||
where T : IBaseRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Register a new repository to the container.
|
||||
/// </summary>
|
||||
/// <param name="builder">The container</param>
|
||||
/// <typeparam name="T">The type of the repository.</typeparam>
|
||||
/// <remarks>
|
||||
/// If your repository implements a special interface, please use <see cref="RegisterRepository{T,T2}"/>
|
||||
/// </remarks>
|
||||
/// <returns>The initial container.</returns>
|
||||
public static IRegistrationBuilder<
|
||||
T,
|
||||
ConcreteReflectionActivatorData,
|
||||
SingleRegistrationStyle
|
||||
> RegisterRepository<T>(this ContainerBuilder builder)
|
||||
where T : IBaseRepository
|
||||
{
|
||||
return builder
|
||||
.RegisterType<T>()
|
||||
.AsSelf()
|
||||
.As<IBaseRepository>()
|
||||
.As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>))!)
|
||||
.InstancePerLifetimeScope();
|
||||
}
|
||||
return builder
|
||||
.RegisterType<T>()
|
||||
.AsSelf()
|
||||
.As<IBaseRepository>()
|
||||
.As(Utility.GetGenericDefinition(typeof(T), typeof(IRepository<>))!)
|
||||
.InstancePerLifetimeScope();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a new repository with a custom mapping to the container.
|
||||
/// </summary>
|
||||
/// <param name="builder">The container</param>
|
||||
/// <typeparam name="T">The custom mapping you have for your repository.</typeparam>
|
||||
/// <typeparam name="T2">The type of the repository.</typeparam>
|
||||
/// <remarks>
|
||||
/// If your repository does not implements a special interface, please use <see cref="RegisterRepository{T}"/>
|
||||
/// </remarks>
|
||||
/// <returns>The initial container.</returns>
|
||||
public static IRegistrationBuilder<
|
||||
T2,
|
||||
ConcreteReflectionActivatorData,
|
||||
SingleRegistrationStyle
|
||||
> RegisterRepository<T, T2>(this ContainerBuilder builder)
|
||||
where T : notnull
|
||||
where T2 : IBaseRepository, T
|
||||
{
|
||||
return builder.RegisterRepository<T2>().AsSelf().As<T>();
|
||||
}
|
||||
/// <summary>
|
||||
/// Register a new repository with a custom mapping to the container.
|
||||
/// </summary>
|
||||
/// <param name="builder">The container</param>
|
||||
/// <typeparam name="T">The custom mapping you have for your repository.</typeparam>
|
||||
/// <typeparam name="T2">The type of the repository.</typeparam>
|
||||
/// <remarks>
|
||||
/// If your repository does not implements a special interface, please use <see cref="RegisterRepository{T}"/>
|
||||
/// </remarks>
|
||||
/// <returns>The initial container.</returns>
|
||||
public static IRegistrationBuilder<
|
||||
T2,
|
||||
ConcreteReflectionActivatorData,
|
||||
SingleRegistrationStyle
|
||||
> RegisterRepository<T, T2>(this ContainerBuilder builder)
|
||||
where T : notnull
|
||||
where T2 : IBaseRepository, T
|
||||
{
|
||||
return builder.RegisterRepository<T2>().AsSelf().As<T>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,56 +18,53 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Kyoo.Utils
|
||||
namespace Kyoo.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// A set of extensions class for enumerable.
|
||||
/// </summary>
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A set of extensions class for enumerable.
|
||||
/// If the enumerable is empty, execute an action.
|
||||
/// </summary>
|
||||
public static class EnumerableExtensions
|
||||
/// <param name="self">The enumerable to check</param>
|
||||
/// <param name="action">The action to execute is the list is empty</param>
|
||||
/// <typeparam name="T">The type of items inside the list</typeparam>
|
||||
/// <returns>The iterator proxied, there is no dual iterations.</returns>
|
||||
public static IEnumerable<T> IfEmpty<T>(this IEnumerable<T> self, Action action)
|
||||
{
|
||||
/// <summary>
|
||||
/// If the enumerable is empty, execute an action.
|
||||
/// </summary>
|
||||
/// <param name="self">The enumerable to check</param>
|
||||
/// <param name="action">The action to execute is the list is empty</param>
|
||||
/// <typeparam name="T">The type of items inside the list</typeparam>
|
||||
/// <returns>The iterator proxied, there is no dual iterations.</returns>
|
||||
[LinqTunnel]
|
||||
public static IEnumerable<T> IfEmpty<T>(this IEnumerable<T> self, Action action)
|
||||
static IEnumerable<T> Generator(IEnumerable<T> self, Action action)
|
||||
{
|
||||
static IEnumerable<T> Generator(IEnumerable<T> self, Action action)
|
||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
using IEnumerator<T> enumerator = self.GetEnumerator();
|
||||
|
||||
if (!enumerator.MoveNext())
|
||||
{
|
||||
action();
|
||||
yield break;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
yield return enumerator.Current;
|
||||
} while (enumerator.MoveNext());
|
||||
action();
|
||||
yield break;
|
||||
}
|
||||
|
||||
return Generator(self, action);
|
||||
do
|
||||
{
|
||||
yield return enumerator.Current;
|
||||
} while (enumerator.MoveNext());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A foreach used as a function with a little specificity: the list can be null.
|
||||
/// </summary>
|
||||
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||
/// <param name="action">The action to execute for each arguments</param>
|
||||
/// <typeparam name="T">The type of items in the list</typeparam>
|
||||
public static void ForEach<T>(this IEnumerable<T>? self, Action<T> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
foreach (T i in self)
|
||||
action(i);
|
||||
}
|
||||
return Generator(self, action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A foreach used as a function with a little specificity: the list can be null.
|
||||
/// </summary>
|
||||
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
|
||||
/// <param name="action">The action to execute for each arguments</param>
|
||||
/// <typeparam name="T">The type of items in the list</typeparam>
|
||||
public static void ForEach<T>(this IEnumerable<T>? self, Action<T> action)
|
||||
{
|
||||
if (self == null)
|
||||
return;
|
||||
foreach (T i in self)
|
||||
action(i);
|
||||
}
|
||||
}
|
||||
|
||||
79
back/src/Kyoo.Abstractions/Utility/JsonKindResolver.cs
Normal file
79
back/src/Kyoo.Abstractions/Utility/JsonKindResolver.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using static System.Text.Json.JsonNamingPolicy;
|
||||
|
||||
namespace Kyoo.Utils;
|
||||
|
||||
public class JsonKindResolver : DefaultJsonTypeInfoResolver
|
||||
{
|
||||
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
|
||||
{
|
||||
JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);
|
||||
|
||||
if (jsonTypeInfo.Type.GetCustomAttribute<OneOfAttribute>() != null)
|
||||
{
|
||||
jsonTypeInfo.PolymorphismOptions = new()
|
||||
{
|
||||
TypeDiscriminatorPropertyName = "kind",
|
||||
IgnoreUnrecognizedTypeDiscriminators = true,
|
||||
DerivedTypes = { },
|
||||
};
|
||||
IEnumerable<Type> derived = AppDomain
|
||||
.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(s => s.GetTypes())
|
||||
.Where(p => type.IsAssignableFrom(p) && p.IsClass);
|
||||
foreach (Type der in derived)
|
||||
{
|
||||
jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(
|
||||
new JsonDerivedType(der, CamelCase.ConvertName(der.Name))
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (
|
||||
jsonTypeInfo.Type.IsAssignableTo(typeof(IResource))
|
||||
&& jsonTypeInfo.Properties.All(x => x.Name != "kind")
|
||||
)
|
||||
{
|
||||
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
|
||||
{
|
||||
TypeDiscriminatorPropertyName = "kind",
|
||||
IgnoreUnrecognizedTypeDiscriminators = true,
|
||||
DerivedTypes =
|
||||
{
|
||||
new JsonDerivedType(
|
||||
jsonTypeInfo.Type,
|
||||
CamelCase.ConvertName(jsonTypeInfo.Type.Name)
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return jsonTypeInfo;
|
||||
}
|
||||
}
|
||||
@@ -20,122 +20,114 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
|
||||
namespace Kyoo.Utils
|
||||
namespace Kyoo.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// A class containing helper methods to merge objects.
|
||||
/// </summary>
|
||||
public static class Merger
|
||||
{
|
||||
/// <summary>
|
||||
/// A class containing helper methods to merge objects.
|
||||
/// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept.
|
||||
/// </summary>
|
||||
public static class Merger
|
||||
/// <param name="first">The first dictionary to merge</param>
|
||||
/// <param name="second">The second dictionary to merge</param>
|
||||
/// <param name="hasChanged">
|
||||
/// <c>true</c> if a new items has been added to the dictionary, <c>false</c> otherwise.
|
||||
/// </param>
|
||||
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
|
||||
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
|
||||
/// <returns>
|
||||
/// A dictionary with the missing elements of <paramref name="second"/>
|
||||
/// set to those of <paramref name="first"/>.
|
||||
/// </returns>
|
||||
public static IDictionary<T, T2>? CompleteDictionaries<T, T2>(
|
||||
IDictionary<T, T2>? first,
|
||||
IDictionary<T, T2>? second,
|
||||
out bool hasChanged
|
||||
)
|
||||
{
|
||||
/// <summary>
|
||||
/// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept.
|
||||
/// </summary>
|
||||
/// <param name="first">The first dictionary to merge</param>
|
||||
/// <param name="second">The second dictionary to merge</param>
|
||||
/// <param name="hasChanged">
|
||||
/// <c>true</c> if a new items has been added to the dictionary, <c>false</c> otherwise.
|
||||
/// </param>
|
||||
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
|
||||
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
|
||||
/// <returns>
|
||||
/// A dictionary with the missing elements of <paramref name="second"/>
|
||||
/// set to those of <paramref name="first"/>.
|
||||
/// </returns>
|
||||
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
|
||||
public static IDictionary<T, T2>? CompleteDictionaries<T, T2>(
|
||||
IDictionary<T, T2>? first,
|
||||
IDictionary<T, T2>? second,
|
||||
out bool hasChanged
|
||||
)
|
||||
if (first == null)
|
||||
{
|
||||
if (first == null)
|
||||
{
|
||||
hasChanged = true;
|
||||
return second;
|
||||
}
|
||||
|
||||
hasChanged = false;
|
||||
if (second == null)
|
||||
return first;
|
||||
hasChanged = second.Any(
|
||||
x => !first.ContainsKey(x.Key) || x.Value?.Equals(first[x.Key]) == false
|
||||
);
|
||||
foreach ((T key, T2 value) in first)
|
||||
second.TryAdd(key, value);
|
||||
hasChanged = true;
|
||||
return second;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set every non-default values of seconds to the corresponding property of second.
|
||||
/// Dictionaries are handled like anonymous objects with a property per key/pair value
|
||||
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"}
|
||||
/// </example>
|
||||
/// <param name="first">
|
||||
/// The object to complete
|
||||
/// </param>
|
||||
/// <param name="second">
|
||||
/// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
|
||||
/// </param>
|
||||
/// <param name="where">
|
||||
/// Filter fields that will be merged
|
||||
/// </param>
|
||||
/// <typeparam name="T">Fields of T will be completed</typeparam>
|
||||
/// <returns><paramref name="first"/></returns>
|
||||
public static T Complete<T>(
|
||||
T first,
|
||||
T? second,
|
||||
[InstantHandle] Func<PropertyInfo, bool>? where = null
|
||||
)
|
||||
{
|
||||
if (second == null)
|
||||
return first;
|
||||
|
||||
Type type = typeof(T);
|
||||
IEnumerable<PropertyInfo> properties = type.GetProperties()
|
||||
.Where(
|
||||
x =>
|
||||
x is { CanRead: true, CanWrite: true }
|
||||
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null
|
||||
);
|
||||
|
||||
if (where != null)
|
||||
properties = properties.Where(where);
|
||||
|
||||
foreach (PropertyInfo property in properties)
|
||||
{
|
||||
object? value = property.GetValue(second);
|
||||
|
||||
if (value?.Equals(property.GetValue(first)) == true)
|
||||
continue;
|
||||
|
||||
if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
|
||||
{
|
||||
Type[] dictionaryTypes = Utility
|
||||
.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))!
|
||||
.GenericTypeArguments;
|
||||
object?[] parameters = { property.GetValue(first), value, false };
|
||||
object newDictionary = Utility.RunGenericMethod<object>(
|
||||
typeof(Merger),
|
||||
nameof(CompleteDictionaries),
|
||||
dictionaryTypes,
|
||||
parameters
|
||||
)!;
|
||||
if ((bool)parameters[2]!)
|
||||
property.SetValue(first, newDictionary);
|
||||
}
|
||||
else
|
||||
property.SetValue(first, value);
|
||||
}
|
||||
|
||||
if (first is IOnMerge merge)
|
||||
merge.OnMerge(second);
|
||||
hasChanged = false;
|
||||
if (second == null)
|
||||
return first;
|
||||
hasChanged = second.Any(x =>
|
||||
!first.ContainsKey(x.Key) || x.Value?.Equals(first[x.Key]) == false
|
||||
);
|
||||
foreach ((T key, T2 value) in first)
|
||||
second.TryAdd(key, value);
|
||||
return second;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set every non-default values of seconds to the corresponding property of second.
|
||||
/// Dictionaries are handled like anonymous objects with a property per key/pair value
|
||||
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"}
|
||||
/// </example>
|
||||
/// <param name="first">
|
||||
/// The object to complete
|
||||
/// </param>
|
||||
/// <param name="second">
|
||||
/// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
|
||||
/// </param>
|
||||
/// <param name="where">
|
||||
/// Filter fields that will be merged
|
||||
/// </param>
|
||||
/// <typeparam name="T">Fields of T will be completed</typeparam>
|
||||
/// <returns><paramref name="first"/></returns>
|
||||
public static T Complete<T>(T first, T? second, Func<PropertyInfo, bool>? where = null)
|
||||
{
|
||||
if (second == null)
|
||||
return first;
|
||||
|
||||
Type type = typeof(T);
|
||||
IEnumerable<PropertyInfo> properties = type.GetProperties()
|
||||
.Where(x =>
|
||||
x is { CanRead: true, CanWrite: true }
|
||||
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null
|
||||
);
|
||||
|
||||
if (where != null)
|
||||
properties = properties.Where(where);
|
||||
|
||||
foreach (PropertyInfo property in properties)
|
||||
{
|
||||
object? value = property.GetValue(second);
|
||||
|
||||
if (value?.Equals(property.GetValue(first)) == true)
|
||||
continue;
|
||||
|
||||
if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
|
||||
{
|
||||
Type[] dictionaryTypes = Utility
|
||||
.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))!
|
||||
.GenericTypeArguments;
|
||||
object?[] parameters = { property.GetValue(first), value, false };
|
||||
object newDictionary = Utility.RunGenericMethod<object>(
|
||||
typeof(Merger),
|
||||
nameof(CompleteDictionaries),
|
||||
dictionaryTypes,
|
||||
parameters
|
||||
)!;
|
||||
if ((bool)parameters[2]!)
|
||||
property.SetValue(first, newDictionary);
|
||||
}
|
||||
else
|
||||
property.SetValue(first, value);
|
||||
}
|
||||
|
||||
if (first is IOnMerge merge)
|
||||
merge.OnMerge(second);
|
||||
return first;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,347 +22,351 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Kyoo.Utils
|
||||
namespace Kyoo.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// A set of utility functions that can be used everywhere.
|
||||
/// </summary>
|
||||
public static class Utility
|
||||
{
|
||||
public static readonly JsonSerializerOptions JsonOptions =
|
||||
new()
|
||||
{
|
||||
TypeInfoResolver = new JsonKindResolver(),
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A set of utility functions that can be used everywhere.
|
||||
/// Convert a string to snake case. Stollen from
|
||||
/// https://github.com/efcore/EFCore.NamingConventions/blob/main/EFCore.NamingConventions/Internal/SnakeCaseNameRewriter.cs
|
||||
/// </summary>
|
||||
public static class Utility
|
||||
/// <param name="name">The string to convert.</param>
|
||||
/// <returns>The string in snake case</returns>
|
||||
public static string ToSnakeCase(this string name)
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert a string to snake case. Stollen from
|
||||
/// https://github.com/efcore/EFCore.NamingConventions/blob/main/EFCore.NamingConventions/Internal/SnakeCaseNameRewriter.cs
|
||||
/// </summary>
|
||||
/// <param name="name">The string to convert.</param>
|
||||
/// <returns>The string in snake case</returns>
|
||||
public static string ToSnakeCase(this string name)
|
||||
StringBuilder builder = new(name.Length + Math.Min(2, name.Length / 5));
|
||||
UnicodeCategory? previousCategory = default;
|
||||
|
||||
for (int currentIndex = 0; currentIndex < name.Length; currentIndex++)
|
||||
{
|
||||
StringBuilder builder = new(name.Length + Math.Min(2, name.Length / 5));
|
||||
UnicodeCategory? previousCategory = default;
|
||||
|
||||
for (int currentIndex = 0; currentIndex < name.Length; currentIndex++)
|
||||
char currentChar = name[currentIndex];
|
||||
if (currentChar == '_')
|
||||
{
|
||||
char currentChar = name[currentIndex];
|
||||
if (currentChar == '_')
|
||||
{
|
||||
builder.Append('_');
|
||||
previousCategory = null;
|
||||
continue;
|
||||
}
|
||||
builder.Append('_');
|
||||
previousCategory = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
UnicodeCategory currentCategory = char.GetUnicodeCategory(currentChar);
|
||||
switch (currentCategory)
|
||||
{
|
||||
case UnicodeCategory.UppercaseLetter:
|
||||
case UnicodeCategory.TitlecaseLetter:
|
||||
if (
|
||||
previousCategory == UnicodeCategory.SpaceSeparator
|
||||
|| previousCategory == UnicodeCategory.LowercaseLetter
|
||||
|| (
|
||||
previousCategory != UnicodeCategory.DecimalDigitNumber
|
||||
&& previousCategory != null
|
||||
&& currentIndex > 0
|
||||
&& currentIndex + 1 < name.Length
|
||||
&& char.IsLower(name[currentIndex + 1])
|
||||
)
|
||||
UnicodeCategory currentCategory = char.GetUnicodeCategory(currentChar);
|
||||
switch (currentCategory)
|
||||
{
|
||||
case UnicodeCategory.UppercaseLetter:
|
||||
case UnicodeCategory.TitlecaseLetter:
|
||||
if (
|
||||
previousCategory == UnicodeCategory.SpaceSeparator
|
||||
|| previousCategory == UnicodeCategory.LowercaseLetter
|
||||
|| (
|
||||
previousCategory != UnicodeCategory.DecimalDigitNumber
|
||||
&& previousCategory != null
|
||||
&& currentIndex > 0
|
||||
&& currentIndex + 1 < name.Length
|
||||
&& char.IsLower(name[currentIndex + 1])
|
||||
)
|
||||
{
|
||||
builder.Append('_');
|
||||
}
|
||||
)
|
||||
{
|
||||
builder.Append('_');
|
||||
}
|
||||
|
||||
currentChar = char.ToLowerInvariant(currentChar);
|
||||
break;
|
||||
currentChar = char.ToLowerInvariant(currentChar);
|
||||
break;
|
||||
|
||||
case UnicodeCategory.LowercaseLetter:
|
||||
case UnicodeCategory.DecimalDigitNumber:
|
||||
if (previousCategory == UnicodeCategory.SpaceSeparator)
|
||||
{
|
||||
builder.Append('_');
|
||||
}
|
||||
break;
|
||||
case UnicodeCategory.LowercaseLetter:
|
||||
case UnicodeCategory.DecimalDigitNumber:
|
||||
if (previousCategory == UnicodeCategory.SpaceSeparator)
|
||||
{
|
||||
builder.Append('_');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (previousCategory != null)
|
||||
{
|
||||
previousCategory = UnicodeCategory.SpaceSeparator;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(currentChar);
|
||||
previousCategory = currentCategory;
|
||||
default:
|
||||
if (previousCategory != null)
|
||||
{
|
||||
previousCategory = UnicodeCategory.SpaceSeparator;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
builder.Append(currentChar);
|
||||
previousCategory = currentCategory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the lambda expression a member (like x => x.Body).
|
||||
/// </summary>
|
||||
/// <param name="ex">The expression that should be checked</param>
|
||||
/// <returns>True if the expression is a member, false otherwise</returns>
|
||||
public static bool IsPropertyExpression(LambdaExpression ex)
|
||||
{
|
||||
return ex.Body is MemberExpression
|
||||
|| (
|
||||
ex.Body.NodeType == ExpressionType.Convert
|
||||
&& ((UnaryExpression)ex.Body).Operand is MemberExpression
|
||||
);
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows)
|
||||
/// </summary>
|
||||
/// <param name="ex">The expression</param>
|
||||
/// <returns>The name of the expression</returns>
|
||||
/// <exception cref="ArgumentException">If the expression is not a property, ArgumentException is thrown.</exception>
|
||||
public static string GetPropertyName(LambdaExpression ex)
|
||||
{
|
||||
if (!IsPropertyExpression(ex))
|
||||
throw new ArgumentException($"{ex} is not a property expression.");
|
||||
MemberExpression? member =
|
||||
/// <summary>
|
||||
/// Is the lambda expression a member (like x => x.Body).
|
||||
/// </summary>
|
||||
/// <param name="ex">The expression that should be checked</param>
|
||||
/// <returns>True if the expression is a member, false otherwise</returns>
|
||||
public static bool IsPropertyExpression(LambdaExpression ex)
|
||||
{
|
||||
return ex.Body is MemberExpression
|
||||
|| (
|
||||
ex.Body.NodeType == ExpressionType.Convert
|
||||
? ((UnaryExpression)ex.Body).Operand as MemberExpression
|
||||
: ex.Body as MemberExpression;
|
||||
return member!.Member.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slugify a string (Replace spaces by -, Uniformize accents)
|
||||
/// </summary>
|
||||
/// <param name="str">The string to slugify</param>
|
||||
/// <returns>The slug version of the given string</returns>
|
||||
public static string ToSlug(string str)
|
||||
{
|
||||
str = str.ToLowerInvariant();
|
||||
|
||||
string normalizedString = str.Normalize(NormalizationForm.FormD);
|
||||
StringBuilder stringBuilder = new();
|
||||
foreach (char c in normalizedString)
|
||||
{
|
||||
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
str = stringBuilder.ToString().Normalize(NormalizationForm.FormC);
|
||||
|
||||
str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled);
|
||||
str = Regex.Replace(str, @"[^\w\s\p{Pd}]", string.Empty, RegexOptions.Compiled);
|
||||
str = str.Trim('-', '_');
|
||||
str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled);
|
||||
return str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
|
||||
/// </summary>
|
||||
/// <param name="self">The starting type</param>
|
||||
/// <returns>A list of types</returns>
|
||||
public static IEnumerable<Type> GetInheritanceTree(this Type self)
|
||||
{
|
||||
for (Type? type = self; type != null; type = type.BaseType)
|
||||
yield return type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if <paramref name="type"/> inherit from a generic type <paramref name="genericType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check</param>
|
||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||
/// <returns>True if obj inherit from genericType. False otherwise</returns>
|
||||
public static bool IsOfGenericType(Type type, Type genericType)
|
||||
{
|
||||
if (!genericType.IsGenericType)
|
||||
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
|
||||
|
||||
IEnumerable<Type> types = genericType.IsInterface
|
||||
? type.GetInterfaces()
|
||||
: type.GetInheritanceTree();
|
||||
return types
|
||||
.Prepend(type)
|
||||
.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the generic definition of <paramref name="genericType"/>.
|
||||
/// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string>
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check</param>
|
||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||
/// <returns>The generic definition of genericType that type inherit or null if type does not implement the generic type.</returns>
|
||||
/// <exception cref="ArgumentException"><paramref name="genericType"/> must be a generic type</exception>
|
||||
public static Type? GetGenericDefinition(Type type, Type genericType)
|
||||
{
|
||||
if (!genericType.IsGenericType)
|
||||
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
|
||||
|
||||
IEnumerable<Type> types = genericType.IsInterface
|
||||
? type.GetInterfaces()
|
||||
: type.GetInheritanceTree();
|
||||
return types
|
||||
.Prepend(type)
|
||||
.FirstOrDefault(
|
||||
x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a method from an <see cref="Type"/> with the given name and respect the
|
||||
/// amount of parameters and generic parameters. This works for polymorphic methods.
|
||||
/// </summary>
|
||||
/// <param name="type">
|
||||
/// The type owning the method. For non static methods, this is the <c>this</c>.
|
||||
/// </param>
|
||||
/// <param name="flag">
|
||||
/// The binding flags of the method. This allow you to specify public/private and so on.
|
||||
/// </param>
|
||||
/// <param name="name">
|
||||
/// The name of the method.
|
||||
/// </param>
|
||||
/// <param name="generics">
|
||||
/// The list of generic parameters.
|
||||
/// </param>
|
||||
/// <param name="args">
|
||||
/// The list of parameters.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||
/// <returns>The method handle of the matching method.</returns>
|
||||
[PublicAPI]
|
||||
public static MethodInfo GetMethod(
|
||||
Type type,
|
||||
BindingFlags flag,
|
||||
string name,
|
||||
Type[] generics,
|
||||
object?[] args
|
||||
)
|
||||
{
|
||||
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
|
||||
.Where(x => x.Name == name)
|
||||
.Where(x => x.GetGenericArguments().Length == generics.Length)
|
||||
.Where(x => x.GetParameters().Length == args.Length)
|
||||
.IfEmpty(() =>
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"A method named {name} with "
|
||||
+ $"{args.Length} arguments and {generics.Length} generic "
|
||||
+ $"types could not be found on {type.Name}."
|
||||
);
|
||||
})
|
||||
// TODO this won't work but I don't know why.
|
||||
// .Where(x =>
|
||||
// {
|
||||
// int i = 0;
|
||||
// return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++]));
|
||||
// })
|
||||
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified."))
|
||||
|
||||
// TODO this won't work for Type<T> because T is specified in arguments but not in the parameters type.
|
||||
// .Where(x =>
|
||||
// {
|
||||
// int i = 0;
|
||||
// return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++]));
|
||||
// })
|
||||
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types."))
|
||||
.Take(2)
|
||||
.ToArray();
|
||||
|
||||
if (methods.Length == 1)
|
||||
return methods[0];
|
||||
throw new ArgumentException(
|
||||
$"Multiple methods named {name} match the generics and parameters constraints."
|
||||
&& ((UnaryExpression)ex.Body).Operand is MemberExpression
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run a generic static method for a runtime <see cref="Type"/>.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// To run Merger.MergeLists{T} for a List where you don't know the type at compile type,
|
||||
/// you could do:
|
||||
/// <code lang="C#">
|
||||
/// Utility.RunGenericMethod<object>(
|
||||
/// typeof(Utility),
|
||||
/// nameof(MergeLists),
|
||||
/// enumerableType,
|
||||
/// oldValue, newValue, equalityComparer)
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <param name="owner">The type that owns the method. For non static methods, the type of <c>this</c>.</param>
|
||||
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
|
||||
/// <param name="type">The generic type to run the method with.</param>
|
||||
/// <param name="args">The list of arguments of the method</param>
|
||||
/// <typeparam name="T">
|
||||
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
|
||||
/// </typeparam>
|
||||
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||
/// <returns>The return of the method you wanted to run.</returns>
|
||||
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/>
|
||||
public static T? RunGenericMethod<T>(
|
||||
Type owner,
|
||||
string methodName,
|
||||
Type type,
|
||||
params object[] args
|
||||
)
|
||||
{
|
||||
return RunGenericMethod<T>(owner, methodName, new[] { type }, args);
|
||||
}
|
||||
/// <summary>
|
||||
/// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows)
|
||||
/// </summary>
|
||||
/// <param name="ex">The expression</param>
|
||||
/// <returns>The name of the expression</returns>
|
||||
/// <exception cref="ArgumentException">If the expression is not a property, ArgumentException is thrown.</exception>
|
||||
public static string GetPropertyName(LambdaExpression ex)
|
||||
{
|
||||
if (!IsPropertyExpression(ex))
|
||||
throw new ArgumentException($"{ex} is not a property expression.");
|
||||
MemberExpression? member =
|
||||
ex.Body.NodeType == ExpressionType.Convert
|
||||
? ((UnaryExpression)ex.Body).Operand as MemberExpression
|
||||
: ex.Body as MemberExpression;
|
||||
return member!.Member.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run a generic static method for a multiple runtime <see cref="Type"/>.
|
||||
/// If your generic method only needs one type, see
|
||||
/// <see cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// To run Merger.MergeLists{T} for a List where you don't know the type at compile type,
|
||||
/// you could do:
|
||||
/// <code>
|
||||
/// Utility.RunGenericMethod<object>(
|
||||
/// typeof(Utility),
|
||||
/// nameof(MergeLists),
|
||||
/// enumerableType,
|
||||
/// oldValue, newValue, equalityComparer)
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <param name="owner">The type that owns the method. For non static methods, the type of <c>this</c>.</param>
|
||||
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
|
||||
/// <param name="types">The list of generic types to run the method with.</param>
|
||||
/// <param name="args">The list of arguments of the method</param>
|
||||
/// <typeparam name="T">
|
||||
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
|
||||
/// </typeparam>
|
||||
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||
/// <returns>The return of the method you wanted to run.</returns>
|
||||
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
|
||||
public static T? RunGenericMethod<T>(
|
||||
Type owner,
|
||||
string methodName,
|
||||
Type[] types,
|
||||
params object?[] args
|
||||
)
|
||||
/// <summary>
|
||||
/// Slugify a string (Replace spaces by -, Uniformize accents)
|
||||
/// </summary>
|
||||
/// <param name="str">The string to slugify</param>
|
||||
/// <returns>The slug version of the given string</returns>
|
||||
public static string ToSlug(string str)
|
||||
{
|
||||
str = str.ToLowerInvariant();
|
||||
|
||||
string normalizedString = str.Normalize(NormalizationForm.FormD);
|
||||
StringBuilder stringBuilder = new();
|
||||
foreach (char c in normalizedString)
|
||||
{
|
||||
if (types.Length < 1)
|
||||
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
str = stringBuilder.ToString().Normalize(NormalizationForm.FormC);
|
||||
|
||||
str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled);
|
||||
str = Regex.Replace(str, @"[^\w\s\p{Pd}]", string.Empty, RegexOptions.Compiled);
|
||||
str = str.Trim('-', '_');
|
||||
str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled);
|
||||
return str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
|
||||
/// </summary>
|
||||
/// <param name="self">The starting type</param>
|
||||
/// <returns>A list of types</returns>
|
||||
public static IEnumerable<Type> GetInheritanceTree(this Type self)
|
||||
{
|
||||
for (Type? type = self; type != null; type = type.BaseType)
|
||||
yield return type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if <paramref name="type"/> inherit from a generic type <paramref name="genericType"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check</param>
|
||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||
/// <returns>True if obj inherit from genericType. False otherwise</returns>
|
||||
public static bool IsOfGenericType(Type type, Type genericType)
|
||||
{
|
||||
if (!genericType.IsGenericType)
|
||||
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
|
||||
|
||||
IEnumerable<Type> types = genericType.IsInterface
|
||||
? type.GetInterfaces()
|
||||
: type.GetInheritanceTree();
|
||||
return types
|
||||
.Prepend(type)
|
||||
.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the generic definition of <paramref name="genericType"/>.
|
||||
/// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string>
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check</param>
|
||||
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||
/// <returns>The generic definition of genericType that type inherit or null if type does not implement the generic type.</returns>
|
||||
/// <exception cref="ArgumentException"><paramref name="genericType"/> must be a generic type</exception>
|
||||
public static Type? GetGenericDefinition(Type type, Type genericType)
|
||||
{
|
||||
if (!genericType.IsGenericType)
|
||||
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
|
||||
|
||||
IEnumerable<Type> types = genericType.IsInterface
|
||||
? type.GetInterfaces()
|
||||
: type.GetInheritanceTree();
|
||||
return types
|
||||
.Prepend(type)
|
||||
.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a method from an <see cref="Type"/> with the given name and respect the
|
||||
/// amount of parameters and generic parameters. This works for polymorphic methods.
|
||||
/// </summary>
|
||||
/// <param name="type">
|
||||
/// The type owning the method. For non static methods, this is the <c>this</c>.
|
||||
/// </param>
|
||||
/// <param name="flag">
|
||||
/// The binding flags of the method. This allow you to specify public/private and so on.
|
||||
/// </param>
|
||||
/// <param name="name">
|
||||
/// The name of the method.
|
||||
/// </param>
|
||||
/// <param name="generics">
|
||||
/// The list of generic parameters.
|
||||
/// </param>
|
||||
/// <param name="args">
|
||||
/// The list of parameters.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||
/// <returns>The method handle of the matching method.</returns>
|
||||
public static MethodInfo GetMethod(
|
||||
Type type,
|
||||
BindingFlags flag,
|
||||
string name,
|
||||
Type[] generics,
|
||||
object?[] args
|
||||
)
|
||||
{
|
||||
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
|
||||
.Where(x => x.Name == name)
|
||||
.Where(x => x.GetGenericArguments().Length == generics.Length)
|
||||
.Where(x => x.GetParameters().Length == args.Length)
|
||||
.IfEmpty(() =>
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The {nameof(types)} array is empty. At least one type is needed."
|
||||
$"A method named {name} with "
|
||||
+ $"{args.Length} arguments and {generics.Length} generic "
|
||||
+ $"types could not be found on {type.Name}."
|
||||
);
|
||||
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
|
||||
return (T?)method.MakeGenericMethod(types).Invoke(null, args);
|
||||
}
|
||||
})
|
||||
// TODO this won't work but I don't know why.
|
||||
// .Where(x =>
|
||||
// {
|
||||
// int i = 0;
|
||||
// return x.GetGenericArguments().All(y => y.IsAssignableFrom(generics[i++]));
|
||||
// })
|
||||
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the generics specified."))
|
||||
|
||||
/// <summary>
|
||||
/// Convert a dictionary to a query string.
|
||||
/// </summary>
|
||||
/// <param name="query">The list of query parameters.</param>
|
||||
/// <returns>A valid query string with all items in the dictionary.</returns>
|
||||
public static string ToQueryString(this Dictionary<string, string> query)
|
||||
{
|
||||
if (!query.Any())
|
||||
return string.Empty;
|
||||
return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}"));
|
||||
}
|
||||
// TODO this won't work for Type<T> because T is specified in arguments but not in the parameters type.
|
||||
// .Where(x =>
|
||||
// {
|
||||
// int i = 0;
|
||||
// return x.GetParameters().All(y => y.ParameterType.IsInstanceOfType(args[i++]));
|
||||
// })
|
||||
// .IfEmpty(() => throw new NullReferenceException($"No method {name} match the parameters's types."))
|
||||
.Take(2)
|
||||
.ToArray();
|
||||
|
||||
if (methods.Length == 1)
|
||||
return methods[0];
|
||||
throw new ArgumentException(
|
||||
$"Multiple methods named {name} match the generics and parameters constraints."
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run a generic static method for a runtime <see cref="Type"/>.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// To run Merger.MergeLists{T} for a List where you don't know the type at compile type,
|
||||
/// you could do:
|
||||
/// <code lang="C#">
|
||||
/// Utility.RunGenericMethod<object>(
|
||||
/// typeof(Utility),
|
||||
/// nameof(MergeLists),
|
||||
/// enumerableType,
|
||||
/// oldValue, newValue, equalityComparer)
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <param name="owner">The type that owns the method. For non static methods, the type of <c>this</c>.</param>
|
||||
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
|
||||
/// <param name="type">The generic type to run the method with.</param>
|
||||
/// <param name="args">The list of arguments of the method</param>
|
||||
/// <typeparam name="T">
|
||||
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
|
||||
/// </typeparam>
|
||||
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||
/// <returns>The return of the method you wanted to run.</returns>
|
||||
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type[],object[])"/>
|
||||
public static T? RunGenericMethod<T>(
|
||||
Type owner,
|
||||
string methodName,
|
||||
Type type,
|
||||
params object[] args
|
||||
)
|
||||
{
|
||||
return RunGenericMethod<T>(owner, methodName, new[] { type }, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run a generic static method for a multiple runtime <see cref="Type"/>.
|
||||
/// If your generic method only needs one type, see
|
||||
/// <see cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// To run Merger.MergeLists{T} for a List where you don't know the type at compile type,
|
||||
/// you could do:
|
||||
/// <code>
|
||||
/// Utility.RunGenericMethod<object>(
|
||||
/// typeof(Utility),
|
||||
/// nameof(MergeLists),
|
||||
/// enumerableType,
|
||||
/// oldValue, newValue, equalityComparer)
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <param name="owner">The type that owns the method. For non static methods, the type of <c>this</c>.</param>
|
||||
/// <param name="methodName">The name of the method. You should use the <c>nameof</c> keyword.</param>
|
||||
/// <param name="types">The list of generic types to run the method with.</param>
|
||||
/// <param name="args">The list of arguments of the method</param>
|
||||
/// <typeparam name="T">
|
||||
/// The return type of the method. You can put <see cref="object"/> for an unknown one.
|
||||
/// </typeparam>
|
||||
/// <exception cref="ArgumentException">No method match the given constraints.</exception>
|
||||
/// <returns>The return of the method you wanted to run.</returns>
|
||||
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
|
||||
public static T? RunGenericMethod<T>(
|
||||
Type owner,
|
||||
string methodName,
|
||||
Type[] types,
|
||||
params object?[] args
|
||||
)
|
||||
{
|
||||
if (types.Length < 1)
|
||||
throw new ArgumentException(
|
||||
$"The {nameof(types)} array is empty. At least one type is needed."
|
||||
);
|
||||
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
|
||||
return (T?)method.MakeGenericMethod(types).Invoke(null, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a dictionary to a query string.
|
||||
/// </summary>
|
||||
/// <param name="query">The list of query parameters.</param>
|
||||
/// <returns>A valid query string with all items in the dictionary.</returns>
|
||||
public static string ToQueryString(this Dictionary<string, string> query)
|
||||
{
|
||||
if (!query.Any())
|
||||
return string.Empty;
|
||||
return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Autofac;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Authentication.Models;
|
||||
@@ -25,83 +28,155 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Kyoo.Authentication
|
||||
namespace Kyoo.Authentication;
|
||||
|
||||
/// <summary>
|
||||
/// A module that enable OpenID authentication for Kyoo.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Create a new authentication module instance and use the given configuration.
|
||||
/// </remarks>
|
||||
public class AuthenticationModule(
|
||||
IConfiguration configuration,
|
||||
ILogger<AuthenticationModule> logger
|
||||
) : IPlugin
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name => "Authentication";
|
||||
|
||||
/// <summary>
|
||||
/// A module that enable OpenID authentication for Kyoo.
|
||||
/// The configuration to use.
|
||||
/// </summary>
|
||||
public class AuthenticationModule : IPlugin
|
||||
private readonly IConfiguration _configuration = configuration;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(ContainerBuilder builder)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name => "Authentication";
|
||||
|
||||
/// <summary>
|
||||
/// The configuration to use.
|
||||
/// </summary>
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new authentication module instance and use the given configuration.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration to use</param>
|
||||
public AuthenticationModule(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterType<PermissionValidator>().As<IPermissionValidator>().SingleInstance();
|
||||
builder.RegisterType<TokenController>().As<ITokenController>().SingleInstance();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(IServiceCollection services)
|
||||
{
|
||||
string secret = _configuration.GetValue(
|
||||
"AUTHENTICATION_SECRET",
|
||||
AuthenticationOption.DefaultSecret
|
||||
)!;
|
||||
PermissionOption permissions =
|
||||
new()
|
||||
{
|
||||
Default = _configuration
|
||||
.GetValue("UNLOGGED_PERMISSIONS", "overall.read")!
|
||||
.Split(','),
|
||||
NewUser = _configuration
|
||||
.GetValue("DEFAULT_PERMISSIONS", "overall.read")!
|
||||
.Split(','),
|
||||
ApiKeys = _configuration.GetValue("KYOO_APIKEYS", string.Empty)!.Split(','),
|
||||
};
|
||||
services.AddSingleton(permissions);
|
||||
services.AddSingleton(
|
||||
new AuthenticationOption() { Secret = secret, Permissions = permissions, }
|
||||
);
|
||||
|
||||
// TODO handle direct-videos with bearers (probably add a cookie and a app.Use to translate that for videos)
|
||||
services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret))
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IStartupAction> ConfigureSteps =>
|
||||
new IStartupAction[]
|
||||
{
|
||||
SA.New<IApplicationBuilder>(app => app.UseAuthentication(), SA.Authentication),
|
||||
};
|
||||
builder.RegisterType<PermissionValidator>().As<IPermissionValidator>().SingleInstance();
|
||||
builder.RegisterType<TokenController>().As<ITokenController>().SingleInstance();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(IServiceCollection services)
|
||||
{
|
||||
string secret = _configuration.GetValue(
|
||||
"AUTHENTICATION_SECRET",
|
||||
AuthenticationOption.DefaultSecret
|
||||
)!;
|
||||
PermissionOption options =
|
||||
new()
|
||||
{
|
||||
Default = _configuration
|
||||
.GetValue("UNLOGGED_PERMISSIONS", "")!
|
||||
.Split(',')
|
||||
.Where(x => x.Length > 0)
|
||||
.ToArray(),
|
||||
NewUser = _configuration
|
||||
.GetValue("DEFAULT_PERMISSIONS", "overall.read,overall.play")!
|
||||
.Split(','),
|
||||
RequireVerification = _configuration.GetValue("REQUIRE_ACCOUNT_VERIFICATION", true),
|
||||
PublicUrl =
|
||||
_configuration.GetValue<string?>("PUBLIC_URL") ?? "http://localhost:8901",
|
||||
ApiKeys = _configuration.GetValue("KYOO_APIKEYS", string.Empty)!.Split(','),
|
||||
OIDC = _configuration
|
||||
.AsEnumerable()
|
||||
.Where((pair) => pair.Key.StartsWith("OIDC_"))
|
||||
.Aggregate(
|
||||
new Dictionary<string, OidcProvider>(),
|
||||
(acc, val) =>
|
||||
{
|
||||
if (val.Value is null)
|
||||
return acc;
|
||||
if (val.Key.Split("_") is not ["OIDC", string provider, string key])
|
||||
{
|
||||
logger.LogError("Invalid oidc config value: {Key}", val.Key);
|
||||
return acc;
|
||||
}
|
||||
provider = provider.ToLowerInvariant();
|
||||
key = key.ToLowerInvariant();
|
||||
|
||||
if (!acc.ContainsKey(provider))
|
||||
acc.Add(provider, new(provider));
|
||||
switch (key)
|
||||
{
|
||||
case "clientid":
|
||||
acc[provider].ClientId = val.Value;
|
||||
break;
|
||||
case "secret":
|
||||
acc[provider].Secret = val.Value;
|
||||
break;
|
||||
case "scope":
|
||||
acc[provider].Scope = val.Value;
|
||||
break;
|
||||
case "authorization":
|
||||
acc[provider].AuthorizationUrl = val.Value;
|
||||
break;
|
||||
case "token":
|
||||
acc[provider].TokenUrl = val.Value;
|
||||
break;
|
||||
case "userinfo":
|
||||
case "profile":
|
||||
acc[provider].ProfileUrl = val.Value;
|
||||
break;
|
||||
case "name":
|
||||
acc[provider].DisplayName = val.Value;
|
||||
break;
|
||||
case "logo":
|
||||
acc[provider].LogoUrl = val.Value;
|
||||
break;
|
||||
default:
|
||||
logger.LogError("Invalid oidc config value: {Key}", key);
|
||||
return acc;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
),
|
||||
};
|
||||
services.AddSingleton(options);
|
||||
services.AddSingleton(
|
||||
new AuthenticationOption() { Secret = secret, Permissions = options, }
|
||||
);
|
||||
|
||||
services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Events = new()
|
||||
{
|
||||
OnMessageReceived = (ctx) =>
|
||||
{
|
||||
string prefix = "Bearer ";
|
||||
if (
|
||||
ctx.Request.Headers.TryGetValue("Authorization", out StringValues val)
|
||||
&& val.ToString() is string auth
|
||||
&& auth.StartsWith(prefix)
|
||||
)
|
||||
{
|
||||
ctx.Token ??= auth[prefix.Length..];
|
||||
}
|
||||
ctx.Token ??= ctx.Request.Cookies["X-Bearer"];
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret))
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IStartupAction> ConfigureSteps =>
|
||||
new IStartupAction[]
|
||||
{
|
||||
SA.New<IApplicationBuilder>(app => app.UseAuthentication(), SA.Authentication),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,34 +21,33 @@ using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Kyoo.Authentication
|
||||
namespace Kyoo.Authentication;
|
||||
|
||||
/// <summary>
|
||||
/// The service that controls jwt creation and validation.
|
||||
/// </summary>
|
||||
public interface ITokenController
|
||||
{
|
||||
/// <summary>
|
||||
/// The service that controls jwt creation and validation.
|
||||
/// Create a new access token for the given user.
|
||||
/// </summary>
|
||||
public interface ITokenController
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new access token for the given user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to create a token for.</param>
|
||||
/// <param name="expireIn">When this token will expire.</param>
|
||||
/// <returns>A new, valid access token.</returns>
|
||||
string CreateAccessToken(User user, out TimeSpan expireIn);
|
||||
/// <param name="user">The user to create a token for.</param>
|
||||
/// <param name="expireIn">When this token will expire.</param>
|
||||
/// <returns>A new, valid access token.</returns>
|
||||
string CreateAccessToken(User user, out TimeSpan expireIn);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new refresh token for the given user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to create a token for.</param>
|
||||
/// <returns>A new, valid refresh token.</returns>
|
||||
Task<string> CreateRefreshToken(User user);
|
||||
/// <summary>
|
||||
/// Create a new refresh token for the given user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to create a token for.</param>
|
||||
/// <returns>A new, valid refresh token.</returns>
|
||||
Task<string> CreateRefreshToken(User user);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the given refresh token is valid and if it is, retrieve the id of the user this token belongs to.
|
||||
/// </summary>
|
||||
/// <param name="refreshToken">The refresh token to validate.</param>
|
||||
/// <exception cref="SecurityTokenException">The given refresh token is not valid.</exception>
|
||||
/// <returns>The id of the token's user.</returns>
|
||||
Guid GetRefreshTokenUserID(string refreshToken);
|
||||
}
|
||||
/// <summary>
|
||||
/// Check if the given refresh token is valid and if it is, retrieve the id of the user this token belongs to.
|
||||
/// </summary>
|
||||
/// <param name="refreshToken">The refresh token to validate.</param>
|
||||
/// <exception cref="SecurityTokenException">The given refresh token is not valid.</exception>
|
||||
/// <returns>The id of the token's user.</returns>
|
||||
Guid GetRefreshTokenUserID(string refreshToken);
|
||||
}
|
||||
|
||||
135
back/src/Kyoo.Authentication/Controllers/OidcController.cs
Normal file
135
back/src/Kyoo.Authentication/Controllers/OidcController.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Authentication.Models;
|
||||
using Kyoo.Authentication.Models.DTO;
|
||||
|
||||
namespace Kyoo.Authentication;
|
||||
|
||||
public class OidcController(
|
||||
IUserRepository users,
|
||||
IHttpClientFactory clientFactory,
|
||||
PermissionOption options
|
||||
)
|
||||
{
|
||||
private async Task<(User, ExternalToken)> _TranslateCode(string provider, string code)
|
||||
{
|
||||
OidcProvider prov = options.OIDC[provider];
|
||||
|
||||
HttpClient client = clientFactory.CreateClient();
|
||||
|
||||
string auth = Convert.ToBase64String(
|
||||
Encoding.UTF8.GetBytes($"{prov.ClientId}:{prov.Secret}")
|
||||
);
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Basic {auth}");
|
||||
Dictionary<string, string> data =
|
||||
new()
|
||||
{
|
||||
["code"] = code,
|
||||
["client_id"] = prov.ClientId,
|
||||
["client_secret"] = prov.Secret,
|
||||
["redirect_uri"] = $"{options.PublicUrl.TrimEnd('/')}/api/auth/logged/{provider}",
|
||||
["grant_type"] = "authorization_code",
|
||||
};
|
||||
HttpResponseMessage resp = prov.TokenUseJsonBody
|
||||
? await client.PostAsJsonAsync(prov.TokenUrl, data)
|
||||
: await client.PostAsync(prov.TokenUrl, new FormUrlEncodedContent(data));
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
throw new ValidationException(
|
||||
$"Invalid code or configuration. {resp.StatusCode}: {await resp.Content.ReadAsStringAsync()}"
|
||||
);
|
||||
JwtToken? token = await resp.Content.ReadFromJsonAsync<JwtToken>();
|
||||
if (token is null)
|
||||
throw new ValidationException("Could not retrive token.");
|
||||
|
||||
client.DefaultRequestHeaders.Remove("Authorization");
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"{token.TokenType} {token.AccessToken}");
|
||||
Dictionary<string, string>? extraHeaders = prov.GetExtraHeaders?.Invoke(prov);
|
||||
if (extraHeaders is not null)
|
||||
{
|
||||
foreach ((string key, string value) in extraHeaders)
|
||||
client.DefaultRequestHeaders.Add(key, value);
|
||||
}
|
||||
|
||||
JwtProfile? profile = await client.GetFromJsonAsync<JwtProfile>(prov.ProfileUrl);
|
||||
if (profile is null || profile.Sub is null)
|
||||
throw new ValidationException(
|
||||
$"Missing sub on user object. Got: {JsonSerializer.Serialize(profile)}"
|
||||
);
|
||||
ExternalToken extToken =
|
||||
new()
|
||||
{
|
||||
Id = profile.Sub,
|
||||
Token = token,
|
||||
ProfileUrl = prov.GetProfileUrl?.Invoke(profile),
|
||||
};
|
||||
User newUser = new();
|
||||
if (profile.Email is not null)
|
||||
newUser.Email = profile.Email;
|
||||
if (profile.Username is null)
|
||||
{
|
||||
throw new ValidationException(
|
||||
$"Could not find a username for the user. You may need to add more scopes. Fields: {string.Join(',', profile.Extra)}"
|
||||
);
|
||||
}
|
||||
extToken.Username = profile.Username;
|
||||
newUser.Username = profile.Username;
|
||||
newUser.Slug = Utils.Utility.ToSlug(newUser.Username);
|
||||
newUser.ExternalId.Add(provider, extToken);
|
||||
return (newUser, extToken);
|
||||
}
|
||||
|
||||
public async Task<User> LoginViaCode(string provider, string code)
|
||||
{
|
||||
(User newUser, ExternalToken extToken) = await _TranslateCode(provider, code);
|
||||
User? user = await users.GetByExternalId(provider, extToken.Id);
|
||||
if (user == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
user = await users.Create(newUser);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new ValidationException(
|
||||
"A user already exists with the same username. If this is you, login via username and then link your account."
|
||||
);
|
||||
}
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<User> LinkAccountOrLogin(Guid userId, string provider, string code)
|
||||
{
|
||||
(_, ExternalToken extToken) = await _TranslateCode(provider, code);
|
||||
User? user = await users.GetByExternalId(provider, extToken.Id);
|
||||
if (user != null)
|
||||
return user;
|
||||
return await users.AddExternalToken(userId, provider, extToken);
|
||||
}
|
||||
}
|
||||
@@ -32,257 +32,253 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Kyoo.Authentication
|
||||
namespace Kyoo.Authentication;
|
||||
|
||||
/// <summary>
|
||||
/// A permission validator to validate permission with user Permission array
|
||||
/// or the default array from the configurations if the user is not logged.
|
||||
/// </summary>
|
||||
public class PermissionValidator : IPermissionValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// A permission validator to validate permission with user Permission array
|
||||
/// or the default array from the configurations if the user is not logged.
|
||||
/// The permissions options to retrieve default permissions.
|
||||
/// </summary>
|
||||
public class PermissionValidator : IPermissionValidator
|
||||
private readonly PermissionOption _options;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new factory with the given options.
|
||||
/// </summary>
|
||||
/// <param name="options">The option containing default values.</param>
|
||||
public PermissionValidator(PermissionOption options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFilterMetadata Create(PermissionAttribute attribute)
|
||||
{
|
||||
return new PermissionValidatorFilter(
|
||||
attribute.Type,
|
||||
attribute.Kind,
|
||||
attribute.Group,
|
||||
_options
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFilterMetadata Create(PartialPermissionAttribute attribute)
|
||||
{
|
||||
return new PermissionValidatorFilter(
|
||||
((object?)attribute.Type ?? attribute.Kind)!,
|
||||
attribute.Group,
|
||||
_options
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The authorization filter used by <see cref="PermissionValidator"/>.
|
||||
/// </summary>
|
||||
private class PermissionValidatorFilter : IAsyncAuthorizationFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// The permission to validate.
|
||||
/// </summary>
|
||||
private readonly string? _permission;
|
||||
|
||||
/// <summary>
|
||||
/// The kind of permission needed.
|
||||
/// </summary>
|
||||
private readonly Kind? _kind;
|
||||
|
||||
/// <summary>
|
||||
/// The group of he permission.
|
||||
/// </summary>
|
||||
private Group _group;
|
||||
|
||||
/// <summary>
|
||||
/// The permissions options to retrieve default permissions.
|
||||
/// </summary>
|
||||
private readonly PermissionOption _options;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new factory with the given options.
|
||||
/// Create a new permission validator with the given options.
|
||||
/// </summary>
|
||||
/// <param name="permission">The permission to validate.</param>
|
||||
/// <param name="kind">The kind of permission needed.</param>
|
||||
/// <param name="group">The group of the permission.</param>
|
||||
/// <param name="options">The option containing default values.</param>
|
||||
public PermissionValidator(PermissionOption options)
|
||||
public PermissionValidatorFilter(
|
||||
string permission,
|
||||
Kind kind,
|
||||
Group group,
|
||||
PermissionOption options
|
||||
)
|
||||
{
|
||||
_permission = permission;
|
||||
_kind = kind;
|
||||
_group = group;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new permission validator with the given options.
|
||||
/// </summary>
|
||||
/// <param name="partialInfo">The partial permission to validate.</param>
|
||||
/// <param name="group">The group of the permission.</param>
|
||||
/// <param name="options">The option containing default values.</param>
|
||||
public PermissionValidatorFilter(object partialInfo, Group? group, PermissionOption options)
|
||||
{
|
||||
switch (partialInfo)
|
||||
{
|
||||
case Kind kind:
|
||||
_kind = kind;
|
||||
break;
|
||||
case string perm:
|
||||
_permission = perm;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException(
|
||||
$"{nameof(partialInfo)} can only be a permission string or a kind."
|
||||
);
|
||||
}
|
||||
|
||||
if (group is not null and not Group.None)
|
||||
_group = group.Value;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFilterMetadata Create(PermissionAttribute attribute)
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
return new PermissionValidatorFilter(
|
||||
attribute.Type,
|
||||
attribute.Kind,
|
||||
attribute.Group,
|
||||
_options
|
||||
);
|
||||
}
|
||||
string? permission = _permission;
|
||||
Kind? kind = _kind;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFilterMetadata Create(PartialPermissionAttribute attribute)
|
||||
{
|
||||
return new PermissionValidatorFilter(
|
||||
((object?)attribute.Type ?? attribute.Kind)!,
|
||||
attribute.Group,
|
||||
_options
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The authorization filter used by <see cref="PermissionValidator"/>.
|
||||
/// </summary>
|
||||
private class PermissionValidatorFilter : IAsyncAuthorizationFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// The permission to validate.
|
||||
/// </summary>
|
||||
private readonly string? _permission;
|
||||
|
||||
/// <summary>
|
||||
/// The kind of permission needed.
|
||||
/// </summary>
|
||||
private readonly Kind? _kind;
|
||||
|
||||
/// <summary>
|
||||
/// The group of he permission.
|
||||
/// </summary>
|
||||
private readonly Group _group = Group.Overall;
|
||||
|
||||
/// <summary>
|
||||
/// The permissions options to retrieve default permissions.
|
||||
/// </summary>
|
||||
private readonly PermissionOption _options;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new permission validator with the given options.
|
||||
/// </summary>
|
||||
/// <param name="permission">The permission to validate.</param>
|
||||
/// <param name="kind">The kind of permission needed.</param>
|
||||
/// <param name="group">The group of the permission.</param>
|
||||
/// <param name="options">The option containing default values.</param>
|
||||
public PermissionValidatorFilter(
|
||||
string permission,
|
||||
Kind kind,
|
||||
Group group,
|
||||
PermissionOption options
|
||||
)
|
||||
if (permission == null || kind == null)
|
||||
{
|
||||
_permission = permission;
|
||||
_kind = kind;
|
||||
_group = group;
|
||||
_options = options;
|
||||
}
|
||||
if (context.HttpContext.Items["PermissionGroup"] is Group group and not Group.None)
|
||||
_group = group;
|
||||
else if (_group == Group.None)
|
||||
_group = Group.Overall;
|
||||
else
|
||||
context.HttpContext.Items["PermissionGroup"] = _group;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new permission validator with the given options.
|
||||
/// </summary>
|
||||
/// <param name="partialInfo">The partial permission to validate.</param>
|
||||
/// <param name="group">The group of the permission.</param>
|
||||
/// <param name="options">The option containing default values.</param>
|
||||
public PermissionValidatorFilter(
|
||||
object partialInfo,
|
||||
Group? group,
|
||||
PermissionOption options
|
||||
)
|
||||
{
|
||||
switch (partialInfo)
|
||||
switch (context.HttpContext.Items["PermissionType"])
|
||||
{
|
||||
case Kind kind:
|
||||
_kind = kind;
|
||||
break;
|
||||
case string perm:
|
||||
_permission = perm;
|
||||
permission = perm;
|
||||
break;
|
||||
case Kind kin:
|
||||
kind = kin;
|
||||
break;
|
||||
case null when kind != null:
|
||||
context.HttpContext.Items["PermissionType"] = kind;
|
||||
return;
|
||||
case null when permission != null:
|
||||
context.HttpContext.Items["PermissionType"] = permission;
|
||||
return;
|
||||
default:
|
||||
throw new ArgumentException(
|
||||
$"{nameof(partialInfo)} can only be a permission string or a kind."
|
||||
"Multiple non-matching partial permission attribute "
|
||||
+ "are not supported."
|
||||
);
|
||||
}
|
||||
|
||||
if (group != null)
|
||||
_group = group.Value;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
string? permission = _permission;
|
||||
Kind? kind = _kind;
|
||||
|
||||
if (permission == null || kind == null)
|
||||
{
|
||||
switch (context.HttpContext.Items["PermissionType"])
|
||||
{
|
||||
case string perm:
|
||||
permission = perm;
|
||||
break;
|
||||
case Kind kin:
|
||||
kind = kin;
|
||||
break;
|
||||
case null when kind != null:
|
||||
context.HttpContext.Items["PermissionType"] = kind;
|
||||
return;
|
||||
case null when permission != null:
|
||||
context.HttpContext.Items["PermissionType"] = permission;
|
||||
return;
|
||||
default:
|
||||
throw new ArgumentException(
|
||||
"Multiple non-matching partial permission attribute "
|
||||
+ "are not supported."
|
||||
);
|
||||
}
|
||||
if (permission == null || kind == null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The permission type or kind is still missing after two partial "
|
||||
+ "permission attributes, this is unsupported."
|
||||
);
|
||||
}
|
||||
throw new ArgumentException(
|
||||
"The permission type or kind is still missing after two partial "
|
||||
+ "permission attributes, this is unsupported."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
string permStr = $"{permission.ToLower()}.{kind.ToString()!.ToLower()}";
|
||||
string overallStr = $"{_group.ToString().ToLower()}.{kind.ToString()!.ToLower()}";
|
||||
AuthenticateResult res = _ApiKeyCheck(context);
|
||||
if (res.None)
|
||||
res = await _JwtCheck(context);
|
||||
string permStr = $"{permission.ToLower()}.{kind.ToString()!.ToLower()}";
|
||||
string overallStr = $"{_group.ToString().ToLower()}.{kind.ToString()!.ToLower()}";
|
||||
AuthenticateResult res = _ApiKeyCheck(context);
|
||||
if (res.None)
|
||||
res = await _JwtCheck(context);
|
||||
|
||||
if (res.Succeeded)
|
||||
{
|
||||
ICollection<string> permissions = res.Principal.GetPermissions();
|
||||
if (permissions.All(x => x != permStr && x != overallStr))
|
||||
context.Result = _ErrorResult(
|
||||
$"Missing permission {permStr} or {overallStr}",
|
||||
StatusCodes.Status403Forbidden
|
||||
);
|
||||
}
|
||||
else if (res.None)
|
||||
{
|
||||
ICollection<string> permissions = _options.Default ?? Array.Empty<string>();
|
||||
if (permissions.All(x => x != permStr && x != overallStr))
|
||||
{
|
||||
context.Result = _ErrorResult(
|
||||
$"Unlogged user does not have permission {permStr} or {overallStr}",
|
||||
StatusCodes.Status401Unauthorized
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (res.Failure != null)
|
||||
if (res.Succeeded)
|
||||
{
|
||||
ICollection<string> permissions = res.Principal.GetPermissions();
|
||||
if (permissions.All(x => x != permStr && x != overallStr))
|
||||
context.Result = _ErrorResult(
|
||||
res.Failure.Message,
|
||||
$"Missing permission {permStr} or {overallStr}",
|
||||
StatusCodes.Status403Forbidden
|
||||
);
|
||||
else
|
||||
}
|
||||
else if (res.None)
|
||||
{
|
||||
ICollection<string> permissions = _options.Default ?? Array.Empty<string>();
|
||||
if (permissions.All(x => x != permStr && x != overallStr))
|
||||
{
|
||||
context.Result = _ErrorResult(
|
||||
"Authentication panic",
|
||||
StatusCodes.Status500InternalServerError
|
||||
$"Unlogged user does not have permission {permStr} or {overallStr}",
|
||||
StatusCodes.Status401Unauthorized
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticateResult _ApiKeyCheck(ActionContext context)
|
||||
{
|
||||
if (
|
||||
!context
|
||||
.HttpContext
|
||||
.Request
|
||||
.Headers
|
||||
.TryGetValue("X-API-Key", out StringValues apiKey)
|
||||
)
|
||||
return AuthenticateResult.NoResult();
|
||||
if (!_options.ApiKeys.Contains<string>(apiKey!))
|
||||
return AuthenticateResult.Fail("Invalid API-Key.");
|
||||
return AuthenticateResult.Success(
|
||||
new AuthenticationTicket(
|
||||
new ClaimsPrincipal(
|
||||
new[]
|
||||
{
|
||||
new ClaimsIdentity(
|
||||
new[]
|
||||
{
|
||||
// TODO: Make permission configurable, for now every APIKEY as all permissions.
|
||||
new Claim(
|
||||
Claims.Permissions,
|
||||
string.Join(',', PermissionOption.Admin)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
),
|
||||
"apikey"
|
||||
)
|
||||
else if (res.Failure != null)
|
||||
context.Result = _ErrorResult(res.Failure.Message, StatusCodes.Status403Forbidden);
|
||||
else
|
||||
context.Result = _ErrorResult(
|
||||
"Authentication panic",
|
||||
StatusCodes.Status500InternalServerError
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<AuthenticateResult> _JwtCheck(ActionContext context)
|
||||
{
|
||||
AuthenticateResult ret = await context
|
||||
.HttpContext
|
||||
.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
|
||||
// Change the failure message to make the API nice to use.
|
||||
if (ret.Failure != null)
|
||||
return AuthenticateResult.Fail(
|
||||
"Invalid JWT token. The token may have expired."
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new action result with the given error message and error code.
|
||||
/// </summary>
|
||||
/// <param name="error">The error message.</param>
|
||||
/// <param name="code">The status code of the error.</param>
|
||||
/// <returns>The resulting error action.</returns>
|
||||
private static IActionResult _ErrorResult(string error, int code)
|
||||
private AuthenticateResult _ApiKeyCheck(ActionContext context)
|
||||
{
|
||||
return new ObjectResult(new RequestError(error)) { StatusCode = code };
|
||||
if (
|
||||
!context.HttpContext.Request.Headers.TryGetValue(
|
||||
"X-API-Key",
|
||||
out StringValues apiKey
|
||||
)
|
||||
)
|
||||
return AuthenticateResult.NoResult();
|
||||
if (!_options.ApiKeys.Contains<string>(apiKey!))
|
||||
return AuthenticateResult.Fail("Invalid API-Key.");
|
||||
return AuthenticateResult.Success(
|
||||
new AuthenticationTicket(
|
||||
new ClaimsPrincipal(
|
||||
new[]
|
||||
{
|
||||
new ClaimsIdentity(
|
||||
new[]
|
||||
{
|
||||
// TODO: Make permission configurable, for now every APIKEY as all permissions.
|
||||
new Claim(
|
||||
Claims.Permissions,
|
||||
string.Join(',', PermissionOption.Admin)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
),
|
||||
"apikey"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<AuthenticateResult> _JwtCheck(ActionContext context)
|
||||
{
|
||||
AuthenticateResult ret = await context.HttpContext.AuthenticateAsync(
|
||||
JwtBearerDefaults.AuthenticationScheme
|
||||
);
|
||||
// Change the failure message to make the API nice to use.
|
||||
if (ret.Failure != null)
|
||||
return AuthenticateResult.Fail("Invalid JWT token. The token may have expired.");
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new action result with the given error message and error code.
|
||||
/// </summary>
|
||||
/// <param name="error">The error message.</param>
|
||||
/// <param name="code">The status code of the error.</param>
|
||||
/// <returns>The resulting error action.</returns>
|
||||
private static IActionResult _ErrorResult(string error, int code)
|
||||
{
|
||||
return new ObjectResult(new RequestError(error)) { StatusCode = code };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,109 +27,108 @@ using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Authentication.Models;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Kyoo.Authentication
|
||||
namespace Kyoo.Authentication;
|
||||
|
||||
/// <summary>
|
||||
/// The service that controls jwt creation and validation.
|
||||
/// </summary>
|
||||
public class TokenController : ITokenController
|
||||
{
|
||||
/// <summary>
|
||||
/// The service that controls jwt creation and validation.
|
||||
/// The options that this controller will use.
|
||||
/// </summary>
|
||||
public class TokenController : ITokenController
|
||||
private readonly AuthenticationOption _options;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="TokenController"/>.
|
||||
/// </summary>
|
||||
/// <param name="options">The options that this controller will use.</param>
|
||||
public TokenController(AuthenticationOption options)
|
||||
{
|
||||
/// <summary>
|
||||
/// The options that this controller will use.
|
||||
/// </summary>
|
||||
private readonly AuthenticationOption _options;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="TokenController"/>.
|
||||
/// </summary>
|
||||
/// <param name="options">The options that this controller will use.</param>
|
||||
public TokenController(AuthenticationOption options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public string CreateAccessToken(User user, out TimeSpan expireIn)
|
||||
{
|
||||
expireIn = new TimeSpan(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CreateAccessToken(User user, out TimeSpan expireIn)
|
||||
{
|
||||
expireIn = new TimeSpan(1, 0, 0);
|
||||
SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret));
|
||||
SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
|
||||
string permissions =
|
||||
user.Permissions != null ? string.Join(',', user.Permissions) : string.Empty;
|
||||
List<Claim> claims =
|
||||
new()
|
||||
{
|
||||
new Claim(Claims.Id, user.Id.ToString()),
|
||||
new Claim(Claims.Name, user.Username),
|
||||
new Claim(Claims.Permissions, permissions),
|
||||
new Claim(Claims.Type, "access")
|
||||
};
|
||||
if (user.Email != null)
|
||||
claims.Add(new Claim(Claims.Email, user.Email));
|
||||
JwtSecurityToken token =
|
||||
new(
|
||||
signingCredentials: credential,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.Add(expireIn)
|
||||
);
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret));
|
||||
SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
|
||||
string permissions =
|
||||
user.Permissions != null ? string.Join(',', user.Permissions) : string.Empty;
|
||||
List<Claim> claims =
|
||||
new()
|
||||
/// <inheritdoc />
|
||||
public Task<string> CreateRefreshToken(User user)
|
||||
{
|
||||
SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret));
|
||||
SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
|
||||
JwtSecurityToken token =
|
||||
new(
|
||||
signingCredentials: credential,
|
||||
claims: new[]
|
||||
{
|
||||
new Claim(Claims.Id, user.Id.ToString()),
|
||||
new Claim(Claims.Name, user.Username),
|
||||
new Claim(Claims.Permissions, permissions),
|
||||
new Claim(Claims.Type, "access")
|
||||
};
|
||||
if (user.Email != null)
|
||||
claims.Add(new Claim(Claims.Email, user.Email));
|
||||
JwtSecurityToken token =
|
||||
new(
|
||||
signingCredentials: credential,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.Add(expireIn)
|
||||
);
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
new Claim(Claims.Guid, Guid.NewGuid().ToString()),
|
||||
new Claim(Claims.Type, "refresh")
|
||||
},
|
||||
expires: DateTime.UtcNow.AddYears(1)
|
||||
);
|
||||
// TODO: refresh keys are unique (thanks to the guid) but we could store them in DB to invalidate them if requested by the user.
|
||||
return Task.FromResult(new JwtSecurityTokenHandler().WriteToken(token));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> CreateRefreshToken(User user)
|
||||
/// <inheritdoc />
|
||||
public Guid GetRefreshTokenUserID(string refreshToken)
|
||||
{
|
||||
SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret));
|
||||
JwtSecurityTokenHandler tokenHandler = new();
|
||||
ClaimsPrincipal principal;
|
||||
try
|
||||
{
|
||||
SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret));
|
||||
SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
|
||||
JwtSecurityToken token =
|
||||
new(
|
||||
signingCredentials: credential,
|
||||
claims: new[]
|
||||
{
|
||||
new Claim(Claims.Id, user.Id.ToString()),
|
||||
new Claim(Claims.Guid, Guid.NewGuid().ToString()),
|
||||
new Claim(Claims.Type, "refresh")
|
||||
},
|
||||
expires: DateTime.UtcNow.AddYears(1)
|
||||
);
|
||||
// TODO: refresh keys are unique (thanks to the guid) but we could store them in DB to invalidate them if requested by the user.
|
||||
return Task.FromResult(new JwtSecurityTokenHandler().WriteToken(token));
|
||||
principal = tokenHandler.ValidateToken(
|
||||
refreshToken,
|
||||
new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateLifetime = true,
|
||||
IssuerSigningKey = key
|
||||
},
|
||||
out SecurityToken _
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetRefreshTokenUserID(string refreshToken)
|
||||
catch (Exception)
|
||||
{
|
||||
SymmetricSecurityKey key = new(Encoding.UTF8.GetBytes(_options.Secret));
|
||||
JwtSecurityTokenHandler tokenHandler = new();
|
||||
ClaimsPrincipal principal;
|
||||
try
|
||||
{
|
||||
principal = tokenHandler.ValidateToken(
|
||||
refreshToken,
|
||||
new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidateLifetime = true,
|
||||
IssuerSigningKey = key
|
||||
},
|
||||
out SecurityToken _
|
||||
);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw new SecurityTokenException("Invalid refresh token");
|
||||
}
|
||||
|
||||
if (principal.Claims.First(x => x.Type == Claims.Type).Value != "refresh")
|
||||
throw new SecurityTokenException(
|
||||
"Invalid token type. The token should be a refresh token."
|
||||
);
|
||||
Claim identifier = principal.Claims.First(x => x.Type == Claims.Id);
|
||||
if (Guid.TryParse(identifier.Value, out Guid id))
|
||||
return id;
|
||||
throw new SecurityTokenException("Token not associated to any user.");
|
||||
throw new SecurityTokenException("Invalid refresh token");
|
||||
}
|
||||
|
||||
if (principal.Claims.First(x => x.Type == Claims.Type).Value != "refresh")
|
||||
throw new SecurityTokenException(
|
||||
"Invalid token type. The token should be a refresh token."
|
||||
);
|
||||
Claim identifier = principal.Claims.First(x => x.Type == Claims.Id);
|
||||
if (Guid.TryParse(identifier.Value, out Guid id))
|
||||
return id;
|
||||
throw new SecurityTokenException("Token not associated to any user.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.12" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.3" />
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
|
||||
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user