Compare commits
831 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec581b5e5f | |||
| b77702c851 | |||
| 2370787471 | |||
| a652d718ca | |||
| 0c707c7989 | |||
| 91655fcb0a | |||
| 6e405af1dc | |||
| 95220e2ed6 | |||
| 56919b9565 | |||
| 25269263ce | |||
| b183bec94b | |||
| bc42ff4b28 | |||
| b501ba9620 | |||
| acab85ada3 | |||
| ad834167d5 | |||
| f59ab11c43 | |||
| 10fed9409d | |||
| f2166df463 | |||
| 3b60289502 | |||
| 2f9b8888e9 | |||
| 65af2bcbd7 | |||
| 2e3d24cc05 | |||
| dcb6ae396c | |||
| c438ec17ed | |||
| f54bc76767 | |||
| baa16b6586 | |||
| d825f8ac81 | |||
| 0148f374ee | |||
| d8c7eb5d3d | |||
| 03ac3bbc00 | |||
| e235673f12 | |||
| b6f7732f13 | |||
| 43b4f143eb | |||
| 9b8748c778 | |||
| 673d186de8 | |||
| 14d843897c | |||
| 3b416c7fb2 | |||
| e3571eb2a4 | |||
| 6ccabd4c72 | |||
| c43992630e | |||
| e23d1a23be | |||
| c8a693ae17 | |||
| e68455f0ed | |||
| 6c073e19dd | |||
| 71787c46d1 | |||
| 35a391e99d | |||
| 85ebd83bdd | |||
| 882faf2b2d | |||
| d7d6cc44de | |||
| 42f7a6e720 | |||
| 96f81b0c1b | |||
| 7d9bd1d3e4 | |||
| 2ffa74e7ea | |||
| 837c34aee2 | |||
| 0fc380b4a9 | |||
| 2644163f57 | |||
| dfe57e484f | |||
| 647c4ad090 | |||
| 6de9d1508f | |||
| b2eb088e0f | |||
| e1e319cb5c | |||
| 59cd5f983b | |||
| 04a7568902 | |||
| 3e7d1158da | |||
| fd016cfb13 | |||
| f898c35b96 | |||
| bbb3a48963 | |||
| 34feb3c04a | |||
| 8d108d2870 | |||
| 2555ab7539 | |||
| edb6cd11da | |||
| a0a42c630e | |||
| 63553803b1 | |||
| 7b638a22bb | |||
| 011eed7622 | |||
| 344a6dcd2c | |||
| 79a6ac6d82 | |||
| ca90347dc2 | |||
| 66d038c403 | |||
| 2ac4dd9980 | |||
| a1910a7b2e | |||
| 9221e22498 | |||
| 006809f02a | |||
| 93345f812e | |||
| 7c463f5fba | |||
| 74ce774eff | |||
| 9528d0e601 | |||
| 84589380e5 | |||
| ffd61de4f3 | |||
| 441a05ac88 | |||
| 44ecc909ff | |||
| 2b0ccf2a30 | |||
| d4a23cd438 | |||
| 7ce911a401 | |||
| e356fe9146 | |||
| 0c05d9d1a9 | |||
| e5bfe562bc | |||
| 51a7d5d08a | |||
| 5acb200c02 | |||
| f8470630be | |||
| bf5fb593e4 | |||
| 9c259027df | |||
| c622a16261 | |||
| 5465e0c694 | |||
| e5cd3381ac | |||
| c80f05fad1 | |||
| 46dd7d8e99 | |||
| 0d9c9c9ecc | |||
| 0b5bbb528a | |||
| f10c84465a | |||
| 610887dc2c | |||
| 5edb369e3b | |||
| fc37151fc4 | |||
| c96ff18e5b | |||
| 3f8315df4e | |||
| 1ccec11b91 | |||
| d18d4f9cd2 | |||
| 978552b1c5 | |||
| f53bb0aae9 | |||
| 24d8290462 | |||
| 849437991a | |||
| 1b184b2b35 | |||
| fb6e075e40 | |||
| 3682840d54 | |||
| a6c0fe57c5 | |||
| f4d2da180d | |||
| b561d6aa59 | |||
| b724e57458 | |||
| 6e4d98e6db | |||
| 4f2f781e68 | |||
| 064de2172e | |||
| c3a7de54f3 | |||
| 5adafb446f | |||
| 3df58ab775 | |||
| 8eb4964599 | |||
| f0297ee9ce | |||
| d73e3d964e | |||
| b3766b00d4 | |||
| 3bb8611995 | |||
| e5dc3b8a54 | |||
| 0e6c52f431 | |||
| ccdc69e3b0 | |||
| c56e14d8fb | |||
| c5f8a93513 | |||
| ea89b6ebc5 | |||
| 76418ec530 | |||
| 50f08d41a4 | |||
| 20678c9a8c | |||
| 145258865a | |||
| 792ce4a391 | |||
| 4910aafa1a | |||
| 726ae93a92 | |||
| 0cec5661e0 | |||
| fe73b1a949 | |||
| 80324b19fb | |||
| 05844d60c1 | |||
| eafe5ba6ed | |||
| 7ccce8e0e7 | |||
| 40ab2c4284 | |||
| d1dda7f6c5 | |||
| ff36560575 | |||
| e9abaa519c | |||
| 359e8069d0 | |||
| 39c2885fd6 | |||
| 7d6633ad1e | |||
| 11adad0e67 | |||
| 420d44f638 | |||
| 9fe5a53e47 | |||
| 2996f726c1 | |||
| 37db4bda53 | |||
| 0d78e44633 | |||
| 4962e4ec33 | |||
| 07e97c0ee9 | |||
| 24288e79a9 | |||
| 27d898e59e | |||
| 95281a2205 | |||
| 5a2f45ab4b | |||
| 4207e89a7d | |||
| 163895b99f | |||
| fbb0f1afbc | |||
| ea5f83328d | |||
| a45e66d43c | |||
| 6ad1e341b1 | |||
| b46e66627c | |||
| 7116d3bb72 | |||
| 99f21f1662 | |||
| 9126de26c0 | |||
| 75df54611a | |||
| ceeeaaab8e | |||
| 6da85a0aaa | |||
| 2aad6047c8 | |||
| 1cc490fb19 | |||
| 904699ba21 | |||
| f682c22b08 | |||
| a68793d08a | |||
| e79e2cba27 | |||
| f3298eee69 | |||
| 88df882061 | |||
| 118940fff8 | |||
| 79c37bfcd5 | |||
| a81b90f570 | |||
| 1d24c1df17 | |||
| 4147a83b93 | |||
| 6978dfc294 | |||
| 8c0775e941 | |||
| 5e621d0e3f | |||
| cefa78fc1d | |||
| 0c560b22ce | |||
| 38093e2952 | |||
| 3fafdbc281 | |||
| 9cc47c4fd6 | |||
| 4c524f033f | |||
| 422b2bb3d9 | |||
| 911ac3769c | |||
| 971be1b658 | |||
| 1cc698a30b | |||
| 4cd24a19d4 | |||
| af8e19b169 | |||
| 75ae31895f | |||
| 484291b0c1 | |||
| e8927bc300 | |||
| 7758beba99 | |||
| 090b610eb1 | |||
| 19235909fe | |||
| c3ed1407ca | |||
| 0b7b53a9fd | |||
| 284c6f00db | |||
| ea2c794cb2 | |||
| e1af8dce05 | |||
| 0d02638e82 | |||
| 682c45090c | |||
| b2ba726635 | |||
| 49112163eb | |||
| 0d4cbb999b | |||
| c04f11bd76 | |||
| 6617a27de3 | |||
| 4477f41857 | |||
| 20b4a59281 | |||
| 42c70fba63 | |||
| 8e8cf57025 | |||
| 587f06dccc | |||
| 7ad5ff6aa7 | |||
| a5880c8622 | |||
| 335d97d868 | |||
| 8e80677bdd | |||
| 4df580f0e8 | |||
| 9fa0533506 | |||
| 9ba91d4583 | |||
| a8da0664a3 | |||
| d6bcad3b59 | |||
| 40fbe4a771 | |||
| 678975fbd7 | |||
| fb50b4df8b | |||
| c15656280e | |||
| c70933a23f | |||
| ad9056c8fc | |||
| 77988e51b6 | |||
| 80fe8c67e9 | |||
| 4029de92a1 | |||
| 19d919f8be | |||
| ef01d88912 | |||
| bd085665d6 | |||
| f586fc063f | |||
| 0b20d7a05b | |||
| 17221057bc | |||
| 04b77ca801 | |||
| b11734d955 | |||
| 4a659fb30c | |||
| c203da245d | |||
| 0f36c4a200 | |||
| c4dec76b9c | |||
| 46be43ad24 | |||
| 3fe35fcbc1 | |||
| 43cdbe856a | |||
| 72405bb146 | |||
| 2bb281ad4c | |||
| a2ab2fe340 | |||
| 7df3c09760 | |||
| d26d3f4967 | |||
| 8718c00a5d | |||
| 0c68b190bc | |||
| 2c26ae6724 | |||
| 300c277816 | |||
| 69b21ddc0e | |||
| 90a0e9d962 | |||
| c539ee7e81 | |||
| f97b7b120d | |||
| 7c148c3c7c | |||
| 4fe980f25b | |||
| f0c5370e27 | |||
| 5f14c12c89 | |||
| 49c78d4624 | |||
| 1b698db422 | |||
| 97d3e2cba6 | |||
| 4680d00d2e | |||
| 30c2603911 | |||
| a74246a000 | |||
| 1e04187b52 | |||
| 499e64b1c3 | |||
| 916c3c9cc3 | |||
| f70e0563ee | |||
| d2f517a858 | |||
| 0ae4a70e1c | |||
| 4de43d36dd | |||
| b1e3cf1341 | |||
| dd7de41878 | |||
| e80fc120bf | |||
| 6982ba99ce | |||
| 8cb7297e27 | |||
| 95d95de6f1 | |||
| 7704346e9c | |||
| 6a021d5dae | |||
| 6ebe66c19d | |||
| 40a449c6f2 | |||
| 06e37a7bf6 | |||
| c77649d21e | |||
| 8f97e690c8 | |||
| b557dcfa45 | |||
| 7b66b32cdf | |||
| 7f3bbd5654 | |||
| a7c0c92019 | |||
| e1a619b79e | |||
| 12226d2212 | |||
| ae8723026d | |||
| a730d8a7ba | |||
| 21fec95b07 | |||
| 7e6709f023 | |||
| a4ff630d1f | |||
| 9a07d4336b | |||
| 2b5625d1c4 | |||
| 9ae6ffe441 | |||
| 36fccdf6d4 | |||
| 4e2fc33a61 | |||
| fa7fdf5884 | |||
| 2deb97bac3 | |||
| 9220c2bd63 | |||
| 2e1e445e47 | |||
| 4920aa871a | |||
| 6d501ba418 | |||
| 2a8d168796 | |||
| 34cc90f5e2 | |||
| a06b11980e | |||
| 4e9713a032 | |||
| f82101332d | |||
| 7b6ae06f3b | |||
| 26fbd1eac0 | |||
| 35e86416af | |||
| c210ea87f2 | |||
| 4a1fcca182 | |||
| f5211a6722 | |||
| 0b77a520fa | |||
| d0e0e291f2 | |||
| 82ecfde2f7 | |||
| 7be16e9f74 | |||
| f441c5f125 | |||
| 0008bde28e | |||
| e6e3099da9 | |||
| d7dd98858e | |||
| 3d3d3c26b8 | |||
| 5f3e938d09 | |||
| 3031aa91ba | |||
| fb763c47bf | |||
| a65b614449 | |||
| 505a6ebf8f | |||
| 8c4dfc0b71 | |||
| 10d108a1f4 | |||
| 428c0f19bc | |||
| afd91a3d9d | |||
| a0b291a463 | |||
| a9e4c826aa | |||
| b189813eb1 | |||
| 7c20ea2708 | |||
| e63c05137a | |||
| c091ffdc6b | |||
| 70ffd25b50 | |||
| 247ee406a9 | |||
| 2218f2931c | |||
| 6bc1c18004 | |||
| eee818094e | |||
| 0ec7cc5f5b | |||
| 871970120d | |||
| 59bef7940b | |||
| 830d93e446 | |||
| d696cea9bc | |||
| e83125c17c | |||
| c5da7db120 | |||
| f9b7f2edf7 | |||
| 4192a4dd32 | |||
| 84ffe668d0 | |||
| d6da6906a4 | |||
| b7e2d21038 | |||
| e120b7f2dd | |||
| 6c37a6ef8b | |||
| 9162c17834 | |||
| 929834c982 | |||
| 5b3da3bcd7 | |||
| 4860f945a7 | |||
| 9f74fd7354 | |||
| 1c5c95ad1d | |||
| dd81e52f81 | |||
| 868ad089f8 | |||
| 2a08a30982 | |||
| 812c819162 | |||
| 234a49903b | |||
| cd5962df50 | |||
| 9d99164c19 | |||
| 2db10787af | |||
| 614e5c7912 | |||
| 17b0453cfb | |||
| d5a5b56484 | |||
| ef1ce3d6eb | |||
| 07fb350333 | |||
| f49a501f71 | |||
| f6ca06e9bf | |||
| ef84408199 | |||
| 0db3759325 | |||
| df1611cf95 | |||
| a6d09815f2 | |||
| 38aa870514 | |||
| d13c65229d | |||
| 61e75599b3 | |||
| b2485af6e9 | |||
| 4adaf7f146 | |||
| 563a00faaa | |||
| 5e33a2d376 | |||
| d9d9efc96e | |||
| dd1380e562 | |||
| cfb00b58a2 | |||
| 990ab18353 | |||
| 3578e9a332 | |||
| f60cfc788b | |||
| e2586eed9b | |||
| 7fbc1ff8c0 | |||
| 5767a3a800 | |||
| 4b51ef2c2f | |||
| f50fc0fa86 | |||
| a817391f67 | |||
| cc877229b1 | |||
| a3e7024355 | |||
| d3c7aa6bd2 | |||
| 40601523e7 | |||
| e3664a9561 | |||
| a1ed4b9001 | |||
| e09492de62 | |||
| d10381a0e2 | |||
| 9596115c45 | |||
| ed0be243e9 | |||
| f502fd312e | |||
| 9d0dffe818 | |||
| 4e2089b6a1 | |||
| 55518c06ee | |||
| a272efb9a2 | |||
| 33a8cdfc0b | |||
| 38c6c5b478 | |||
| d759df03b5 | |||
| e2c1121079 | |||
| fe3585577b | |||
| b3a0611302 | |||
| c20cc766e1 | |||
| 8044534774 | |||
| cac463d4c3 | |||
| 7faa35dab4 | |||
| 24022092ad | |||
| e2e3094d2d | |||
| d669994bf0 | |||
| 45b8f6c4c8 | |||
| 044f651299 | |||
| f865910a90 | |||
| f2360c07a3 | |||
| 4c812c9ba4 | |||
| 26261dbfe7 | |||
| ae90e0e52e | |||
| 72d0895401 | |||
| 705368ee49 | |||
| cbc2c7c323 | |||
| e16c8a07bd | |||
| a7a138db31 | |||
| 76956c0abc | |||
| d4376e0539 | |||
| b94ba9d409 | |||
| 744ca84a8b | |||
| c55fde25a5 | |||
| 030031dcff | |||
| 341c19bace | |||
| e3a8d209b4 | |||
| e816870f67 | |||
| f28acc7fa1 | |||
| b2ae39e0d9 | |||
| 893662ba50 | |||
| 314004bc19 | |||
| 0915a61c19 | |||
| e123a13e38 | |||
| b15d3c9ad3 | |||
| 3da05330b2 | |||
| 8293eb26b9 | |||
| d8fc0a9914 | |||
| 1f4f4acb46 | |||
| d64e18b69a | |||
| 7a4271c85f | |||
| afb716566d | |||
| e5c68e1257 | |||
| a94588497c | |||
| 4cedbe28e3 | |||
| 9a258c089d | |||
| 9ff92f3f0d | |||
| 5f5c71ab75 | |||
| d92e59aa72 | |||
| d593436281 | |||
| 41b8bd4459 | |||
| f6f0fed2d1 | |||
| e2adf2e2bf | |||
| 78b535b229 | |||
| 7215950bd4 | |||
| c435f63d09 | |||
| eed664b7d3 | |||
| 5b550517b2 | |||
| e565073fd5 | |||
| 2b93191714 | |||
| 046225654a | |||
| 0db27d7b1b | |||
| f571cd5a6a | |||
| 79a55327dc | |||
| dbc796b0b0 | |||
| 02d8e7d828 | |||
| 1e4d126cb9 | |||
| f3a1d56c56 | |||
| 67b8684f3c | |||
| b04614e18d | |||
| 689a90d275 | |||
| 766be1e8bb | |||
| f2f66606d7 | |||
| 949fa4b513 | |||
| 40fa2fd390 | |||
| 7f26bd1091 | |||
| 86ac1aaa6b | |||
| 3c9727d36c | |||
| 5a2809e337 | |||
| d1d89dfb12 | |||
| 45ec0ed8b5 | |||
| ebb66f6ca3 | |||
| b85c9186ef | |||
| c606102f6d | |||
| 8b70582561 | |||
| b62fef3c1f | |||
| 71ab342838 | |||
| 83c4681e99 | |||
| 42e52f60ad | |||
| 6ac64c5319 | |||
| 83e0cfd7ee | |||
| 700e53a807 | |||
| 4d1f90b2f7 | |||
| d74babd8f3 | |||
| b7b2700425 | |||
| 04e4505402 | |||
| 95330223f4 | |||
| f28fa563c9 | |||
| d6ce6ae8b9 | |||
| 8f57f35372 | |||
| 9aa79682ba | |||
| d4cddb8a5d | |||
| dc300fae53 | |||
| 70980f09de | |||
| ca0cf763ff | |||
| 3d4c52092e | |||
| fc0af10509 | |||
| 88216e0ec4 | |||
| 474ae50c36 | |||
| 929e1936eb | |||
| 6382563440 | |||
| 733b4ba73a | |||
| 5d580abb08 | |||
| ca1f7af445 | |||
| 0c7428f136 | |||
| 97e666c566 | |||
| d36c8ebce8 | |||
| 5be844e1b7 | |||
| 6f189bf2b8 | |||
| f3ff7a446b | |||
| a3ec1a3712 | |||
| 6bbd6dcd44 | |||
| ce43df6f43 | |||
| 6883cd0969 | |||
| 5221584b06 | |||
| c4ace4ac95 | |||
| 2fcf4084f8 | |||
| 1bfbad2420 | |||
| d4f9c12c22 | |||
| d46539b718 | |||
| 13f49305da | |||
| 7d4cef51f5 | |||
| a9aa2d53ed | |||
| 08b87504b4 | |||
| df27faaa41 | |||
| fe9ea18918 | |||
| e71efc3f97 | |||
| 9817185fa3 | |||
| c2a11f3e68 | |||
| d60ae43b59 | |||
| a187ab18b8 | |||
| cf5268c1e4 | |||
| f405e17c96 | |||
| 67300ec17f | |||
| 2b4a0a2314 | |||
| d84a37e1d2 | |||
| 465b6c5d9e | |||
| b975195252 | |||
| 1c28df61df | |||
| c69b8ccc53 | |||
| 138081bd8b | |||
| ea732b62d8 | |||
| e7c1451b6c | |||
| a49a766424 | |||
| ddf3b9f609 | |||
| 9c6fab8f12 | |||
| 992e591938 | |||
| 365036f9f2 | |||
| 93af601a5f | |||
| 894d837578 | |||
| e79caccab1 | |||
| 25b137e0ab | |||
| 75ef410437 | |||
| 849f0a06c8 | |||
| 2e5fb57052 | |||
| e72bde9c02 | |||
| 35a1ec8a5d | |||
| a0c38131c8 | |||
| 3bc45ff92d | |||
| 2d4d6d8c38 | |||
| 9a99572354 | |||
| b2aa80ce5c | |||
| ff365dae34 | |||
| 52aebfb7d3 | |||
| 66ea1b50e6 | |||
| 3f656ade7a | |||
| 8bf0d372c6 | |||
| 202d7b5829 | |||
| 352e4f3aba | |||
| c5f6d00c94 | |||
| e8d1d94436 | |||
| 50dc37065b | |||
| 7e88b18192 | |||
| 89e914c7f1 | |||
| 1932ac4765 | |||
| ec33c74ec4 | |||
| 2184ed1b16 | |||
| d3907afde7 | |||
| e12d933531 | |||
| c0ba29d917 | |||
| d1fd81c382 | |||
| e038045494 | |||
| e1691e649e | |||
| 8d28497d29 | |||
| fddd4e7e6b | |||
| 0581cd6610 | |||
| 0f1732e5f5 | |||
| 41c2d51d8c | |||
| 29b2361857 | |||
| ce867f9834 | |||
| 4034bf9d7e | |||
| 3d2658fa43 | |||
| 61b19688ff | |||
| e8d72bf6a3 | |||
| 348b14f7b7 | |||
| fda49a5a49 | |||
| 55c00d76bb | |||
| 519d2113eb | |||
| f34f6b6941 | |||
| 6864e108b8 | |||
| 09ba04662a | |||
| 9cd2418095 | |||
| b6a96513de | |||
| ca57166e95 | |||
| 33496c1693 | |||
| b65daeca0b | |||
| 286cc6d720 | |||
| aa4f09c799 | |||
| afd3c0d9f3 | |||
| 5597d8e1a7 | |||
| 0166362258 | |||
| 58c330b63d | |||
| be71295693 | |||
| 8cd3090cee | |||
| 7bf08daeec | |||
| 290463fe7b | |||
| 1b2d9c100a | |||
| caa05c1bf2 | |||
| a37ead86df | |||
| e65aff8bc6 | |||
| 9734494eb6 | |||
| d41e302418 | |||
| 80ba517294 | |||
| 95d08b264f | |||
| 893a849f28 | |||
| 673f617994 | |||
| 644327eb76 | |||
| 10662e75e4 | |||
| a2b1936e73 | |||
| 2df546af6d | |||
| 338b480217 | |||
| 2943bb6fdd | |||
| 94edcbd2d1 | |||
| a8d1cdefac | |||
| a518160a6f | |||
| b56de6493f | |||
| 093cfc3f3b | |||
| 49775b1f6a | |||
| 22d593b8e9 | |||
| 2cb7fb52d2 | |||
| 8433b6d8a4 | |||
| 32d2414de0 | |||
| 317a3a47c3 | |||
| 845b8cdc8f | |||
| c86f6439c5 | |||
| 559e0088e5 | |||
| adaca95590 | |||
| 09a1c31fa3 | |||
| e4b82025b8 | |||
| 78e3702cb0 | |||
| 01b20d3b75 | |||
| 156761405e | |||
| 1805f2259f | |||
| 4c587776d6 | |||
| 8379b4634a | |||
| 9470439cfa | |||
| 18096e48e0 | |||
| f2d0ac7b28 | |||
| 2ccf08f547 | |||
| 1e27f460fe | |||
| 4cdd8c8233 | |||
| 6e60634c9f | |||
| 12c5d6b636 | |||
| b617c62f8e | |||
| 035b5895b0 | |||
| 22da5187c8 | |||
| 5804d6840c | |||
| b50ce1ad6b | |||
| 481ee03f35 | |||
| d91adb5d54 | |||
| ef7f138a4e | |||
| 2e8d9a311b | |||
| 4c5a3fbff3 | |||
| 636908fc4d | |||
| 997362fc97 | |||
| c5147341e3 | |||
| ca33bcebf0 | |||
| d32f487e8e | |||
| fb65f8f853 | |||
| 2a0b90e385 | |||
| dde70fd8a2 | |||
| 98d1d0cb35 | |||
| ba76a8f3ad | |||
| 8cd5652157 | |||
| 8aff4227d9 | |||
| 026f7472cb | |||
| daca285568 | |||
| fbb9a0b2c7 | |||
| 29b3aa8543 | |||
| 94f3725208 | |||
| 0ee81e87be | |||
| c491a918c2 | |||
| 1e7e46cb82 | |||
| 5ae444d96d | |||
| ee7ad83427 | |||
| 921d7d3364 | |||
| f8e012582a | |||
| def5956cd1 | |||
| abfbaca336 | |||
| 6566188e45 | |||
| 078f9584ed | |||
| ee34c75386 | |||
| e8150428b6 | |||
| 4b38e35bbb | |||
| 435bb14bb2 | |||
| 2e5ced5098 | |||
| f4a846aa4d | |||
| 7c1063177f | |||
| 5878b1ffc5 | |||
| 3c3c2aee0d | |||
| 511223aac4 | |||
| 3b2d64995a | |||
| 13c4517a66 | |||
| 177b6464ca | |||
| 5a9a8363f4 | |||
| 49efd68fc7 | |||
| 90a8a26c6e | |||
| 002c83e6f5 | |||
| 7222910b05 | |||
| 097cb87f6f | |||
| 91c3b1617e | |||
| 8f71922734 | |||
| d140630208 | |||
| 63a3e55297 | |||
| c2e5081d64 | |||
| 4187c6f620 | |||
| e7dbb3afec | |||
| f994dd6211 | |||
| da254ee968 | |||
| 4ad3141875 | |||
| b5f0199a25 | |||
| 6bf88c049e | |||
| 40a33da2a5 | |||
| 3596fc0693 | |||
| 93824dad97 | |||
| e5656af1f2 | |||
| c127c10458 | |||
| 7d1824ea27 | |||
| 2966d27c97 | |||
| 618ec4543e | |||
| 0e4031ae52 | |||
| 442af96ed9 | |||
| a305204cfa | |||
| 75f472e6a7 | |||
| cc32e8f7cb | |||
| 14b3085ff1 | |||
| 5691eee4f1 | |||
| 1520a697ad | |||
| 81b8b0ca4a | |||
| ac3fa3c376 | |||
| 7a1c1cd342 | |||
| 70c32a26fa | |||
| 2b94bb54aa | |||
| 0a6e8146be | |||
| 305b0fdca3 | |||
| d738386fe2 | |||
| ca830d5be7 | |||
| a5bc4524d8 | |||
| 175ee12bbc | |||
| a725220c21 | |||
| a245605152 | |||
| f4a53209f4 | |||
| 877251bcae |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "10.0.10",
|
||||
"version": "10.0.11",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '24 2 * * 4'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'csharp' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
@@ -1,159 +0,0 @@
|
||||
name: ABI Compatibility
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
abi-head:
|
||||
name: ABI - HEAD
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: abi-head
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
path: out/
|
||||
|
||||
abi-base:
|
||||
name: ABI - BASE
|
||||
if: ${{ github.base_ref != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Checkout common ancestor
|
||||
env:
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
run: |
|
||||
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
|
||||
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
|
||||
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
|
||||
git checkout --progress --force $ANCESTOR_REF
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: abi-base
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
path: out/
|
||||
|
||||
abi-diff:
|
||||
permissions:
|
||||
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
||||
|
||||
name: ABI - Difference
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- abi-head
|
||||
- abi-base
|
||||
|
||||
steps:
|
||||
- name: Download abi-head
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: abi-head
|
||||
path: abi-head
|
||||
|
||||
- name: Download abi-base
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: abi-base
|
||||
path: abi-base
|
||||
|
||||
- name: Setup ApiCompat
|
||||
run: |
|
||||
dotnet tool install --global Microsoft.DotNet.ApiCompat.Tool
|
||||
|
||||
- name: Run ApiCompat
|
||||
id: diff
|
||||
run: |
|
||||
{
|
||||
echo 'body<<EOF'
|
||||
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
|
||||
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 || true )"
|
||||
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
|
||||
printf "\n${file}\n${COMPAT_OUTPUT}\n"
|
||||
fi
|
||||
done
|
||||
echo EOF
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
direction: last
|
||||
body-includes: abi-diff-workflow-comment
|
||||
|
||||
- name: Reply or edit difference comment (changed)
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.diff.outputs.body != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
body: |
|
||||
<!--abi-diff-workflow-comment-->
|
||||
<details>
|
||||
<summary>ABI Difference</summary>
|
||||
|
||||
```
|
||||
${{ steps.diff.outputs.body }}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
- name: Reply or edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
body: |
|
||||
<!--abi-diff-workflow-comment-->
|
||||
<details>
|
||||
<summary>ABI Difference</summary>
|
||||
|
||||
No changes to the ABI found. See history of this comment for previous changes.
|
||||
|
||||
</details>
|
||||
@@ -1,25 +0,0 @@
|
||||
name: Format
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
# Run formatter against the forked branch, but
|
||||
# do not allow access to secrets
|
||||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
SDK_VERSION: "10.0.x"
|
||||
|
||||
jobs:
|
||||
format-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
dotnet-version: ${{ env.SDK_VERSION }}
|
||||
|
||||
- name: Run DotNet Format
|
||||
run: dotnet format --verify-no-changes --verbosity minimal
|
||||
@@ -1,45 +0,0 @@
|
||||
name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
# Run tests against the forked branch, but
|
||||
# do not allow access to secrets
|
||||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
SDK_VERSION: "10.0.x"
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
strategy:
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
|
||||
fail-fast: false
|
||||
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
dotnet-version: ${{ env.SDK_VERSION }}
|
||||
|
||||
- name: Run DotNet CLI Tests
|
||||
run: >
|
||||
dotnet test Jellyfin.sln
|
||||
--configuration Release
|
||||
--collect:"XPlat Code Coverage"
|
||||
--settings tests/coverletArgs.runsettings
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@049f7ec958c672fd31d5cc1cb01622dc8d2e23ab # v5.5.10
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
reporttypes: "Cobertura"
|
||||
|
||||
# TODO - which action / tool to use to publish code coverage results?
|
||||
# - name: Publish code coverage results
|
||||
@@ -1,63 +0,0 @@
|
||||
name: Commands
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
- edited
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- synchronize
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
rebase:
|
||||
name: Rebase
|
||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
rename:
|
||||
name: Rename
|
||||
if: contains(github.event.comment.body, '@jellyfin-bot rename')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
|
||||
- name: install python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
|
||||
- name: install python packages
|
||||
run: pip install -r rename/requirements.txt
|
||||
|
||||
- name: run rename script
|
||||
run: python3 rename.py
|
||||
working-directory: ./rename
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
ISSUE: ${{ github.event.issue.number }}
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Stale Issue Labeler
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
issues:
|
||||
name: Check for stale issues
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
ascending: true
|
||||
days-before-stale: 120
|
||||
days-before-pr-stale: -1
|
||||
days-before-close: 21
|
||||
days-before-pr-close: -1
|
||||
operations-per-run: 500
|
||||
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
|
||||
stale-issue-label: stale
|
||||
stale-issue-message: |-
|
||||
This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
|
||||
|
||||
If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
|
||||
close-issue-message: |-
|
||||
This issue was closed due to inactivity.
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Check Issue Template
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
jobs:
|
||||
check_issue:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
|
||||
- name: install python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
|
||||
- name: install python packages
|
||||
run: pip install -r main-repo-triage/requirements.txt
|
||||
|
||||
- name: check and comment issue
|
||||
working-directory: ./main-repo-triage
|
||||
run: python3 single_issue_gha.py
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
ISSUE: ${{ github.event.issue.number }}
|
||||
@@ -1,44 +0,0 @@
|
||||
name: OpenAPI Generate
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
required: true
|
||||
type: string
|
||||
repository:
|
||||
required: true
|
||||
type: string
|
||||
artifact:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
main:
|
||||
name: Main
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
repository: ${{ inputs.repository }}
|
||||
|
||||
- name: Configure .NET
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Create File
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter Jellyfin.Server.Integration.Tests.OpenApiSpecTests
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
@@ -1,140 +0,0 @@
|
||||
name: OpenAPI Publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
publish-openapi:
|
||||
name: OpenAPI - Publish Artifact
|
||||
uses: ./.github/workflows/openapi-generate.yml
|
||||
permissions:
|
||||
contents: read
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
repository: ${{ github.repository }}
|
||||
artifact: openapi-head
|
||||
|
||||
publish-unstable:
|
||||
name: OpenAPI - Publish Unstable Spec
|
||||
if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- publish-openapi
|
||||
steps:
|
||||
- name: Set unstable dated version
|
||||
id: version
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Upload openapi.json (unstable) to repository server
|
||||
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
key: "${{ secrets.REPO_KEY }}"
|
||||
source: openapi-head/openapi.json
|
||||
strip_components: 1
|
||||
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||
- name: Move openapi.json (unstable) into place
|
||||
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
key: "${{ secrets.REPO_KEY }}"
|
||||
debug: false
|
||||
script: |
|
||||
if ! test -d /run/workflows; then
|
||||
sudo mkdir -p /run/workflows
|
||||
sudo chown ${{ secrets.REPO_USER }} /run/workflows
|
||||
fi
|
||||
(
|
||||
flock -x -w 300 200 || exit 1
|
||||
TGT_DIR="/srv/repository/main/openapi"
|
||||
LAST_SPEC="$( ls -lt ${TGT_DIR}/unstable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
|
||||
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
|
||||
if diff /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/${LAST_SPEC} &>/dev/null; then
|
||||
rm -r /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
|
||||
exit 0
|
||||
fi
|
||||
# Move new spec into place
|
||||
sudo mv /srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
|
||||
# Delete previous jellyfin-openapi-unstable_previous.json
|
||||
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
||||
# Move current jellyfin-openapi-unstable.json symlink to jellyfin-openapi-unstable_previous.json
|
||||
sudo mv ${TGT_DIR}/jellyfin-openapi-unstable.json ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
||||
# Create new jellyfin-openapi-unstable.json symlink
|
||||
sudo ln -s unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-unstable.json
|
||||
# Check that the previous openapi unstable spec link is correct
|
||||
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-unstable_previous.json )" != "unstable/${LAST_SPEC}" ]]; then
|
||||
sudo rm ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
||||
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
||||
fi
|
||||
) 200>/run/workflows/openapi-unstable.lock
|
||||
|
||||
publish-stable:
|
||||
name: OpenAPI - Publish Stable Spec
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- publish-openapi
|
||||
steps:
|
||||
- name: Set version number
|
||||
id: version
|
||||
run: |-
|
||||
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Upload openapi.json (stable) to repository server
|
||||
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
key: "${{ secrets.REPO_KEY }}"
|
||||
source: openapi-head/openapi.json
|
||||
strip_components: 1
|
||||
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||
- name: Move openapi.json (stable) into place
|
||||
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
||||
with:
|
||||
host: "${{ secrets.REPO_HOST }}"
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
key: "${{ secrets.REPO_KEY }}"
|
||||
debug: false
|
||||
script: |
|
||||
if ! test -d /run/workflows; then
|
||||
sudo mkdir -p /run/workflows
|
||||
sudo chown ${{ secrets.REPO_USER }} /run/workflows
|
||||
fi
|
||||
(
|
||||
flock -x -w 300 200 || exit 1
|
||||
TGT_DIR="/srv/repository/main/openapi"
|
||||
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
|
||||
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
|
||||
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
|
||||
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
|
||||
exit 0
|
||||
fi
|
||||
# Move new spec into place
|
||||
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
|
||||
# Delete previous jellyfin-openapi-stable_previous.json
|
||||
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
|
||||
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||
# Create new jellyfin-openapi-stable.json symlink
|
||||
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
|
||||
# Check that the previous openapi stable spec link is correct
|
||||
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
|
||||
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||
fi
|
||||
) 200>/run/workflows/openapi-stable.lock
|
||||
@@ -1,80 +0,0 @@
|
||||
name: OpenAPI Check
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
ancestor:
|
||||
name: Common Ancestor
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
base_ref: ${{ steps.ancestor.outputs.base_ref }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
fetch-depth: 0
|
||||
- name: Search History
|
||||
id: ancestor
|
||||
run: |
|
||||
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
|
||||
git fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
|
||||
|
||||
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} HEAD)
|
||||
|
||||
echo "ref: ${ANCESTOR_REF}"
|
||||
|
||||
echo "base_ref=${ANCESTOR_REF}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
head:
|
||||
name: Head Artifact
|
||||
uses: ./.github/workflows/openapi-generate.yml
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
artifact: openapi-head
|
||||
|
||||
base:
|
||||
name: Base Artifact
|
||||
uses: ./.github/workflows/openapi-generate.yml
|
||||
needs:
|
||||
- ancestor
|
||||
with:
|
||||
ref: ${{ needs.ancestor.outputs.base_ref }}
|
||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
artifact: openapi-base
|
||||
|
||||
diff:
|
||||
name: Generate Report
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- head
|
||||
- base
|
||||
steps:
|
||||
- name: Download Head
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download Base
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
- name: Detect Changes
|
||||
id: openapi-diff
|
||||
run: |
|
||||
sed -i 's:allOf:oneOf:g' openapi-head/openapi.json
|
||||
sed -i 's:allOf:oneOf:g' openapi-base/openapi.json
|
||||
|
||||
mkdir -p /tmp/openapi-report
|
||||
mv openapi-head/openapi.json /tmp/openapi-report/head.json
|
||||
mv openapi-base/openapi.json /tmp/openapi-report/base.json
|
||||
|
||||
docker run -v /tmp/openapi-report:/data openapitools/openapi-diff:2.1.6 /data/base.json /data/head.json --state -l ERROR --markdown /data/openapi-report.md
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: openapi-report
|
||||
path: /tmp/openapi-report/openapi-report.md
|
||||
@@ -1,59 +0,0 @@
|
||||
name: OpenAPI Report
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- OpenAPI Check
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
name: Generate Metadata
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
outputs:
|
||||
pr_number: ${{ steps.pr_number.outputs.pr_number }}
|
||||
steps:
|
||||
- name: Get Pull Request Number
|
||||
id: pr_number
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
API_RESPONSE=$(gh pr list --repo "${GITHUB_REPOSITORY}" --search "${HEAD_SHA}" --state open --json number)
|
||||
PR_NUMBER=$(echo "${API_RESPONSE}" | jq '.[0].number')
|
||||
|
||||
echo "repository: ${GITHUB_REPOSITORY}"
|
||||
echo "sha: ${HEAD_SHA}"
|
||||
echo "response: ${API_RESPONSE}"
|
||||
echo "pr: ${PR_NUMBER}"
|
||||
|
||||
echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
comment:
|
||||
name: Pull Request Comment
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
needs:
|
||||
- metadata
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- name: Download OpenAPI Report
|
||||
id: download_report
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openapi-report
|
||||
path: openapi-report
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Push Comment
|
||||
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
|
||||
with:
|
||||
github-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
file-path: ${{ steps.download_report.outputs.download-path }}/openapi-report.md
|
||||
pr-number: ${{ needs.metadata.outputs.pr_number }}
|
||||
comment-tag: openapi-report
|
||||
@@ -1,66 +0,0 @@
|
||||
name: Project Automation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
issue_comment:
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
project:
|
||||
name: Project board
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||
steps:
|
||||
- name: Remove from 'Current Release' project
|
||||
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Current Release
|
||||
action: delete
|
||||
column: In progress
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add to 'Release Next' project
|
||||
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Release Next
|
||||
column: In progress
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add to 'Current Release' project
|
||||
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
|
||||
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Current Release
|
||||
column: In progress
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Check number of comments from the team member
|
||||
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
|
||||
id: member_comments
|
||||
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
|
||||
|
||||
- name: Move issue to needs triage
|
||||
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
|
||||
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Issue Triage for Main Repo
|
||||
column: Needs triage
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add issue to triage project
|
||||
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0
|
||||
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
||||
continue-on-error: true
|
||||
with:
|
||||
project: Issue Triage for Main Repo
|
||||
column: Pending response
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Merge Conflict Labeler
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
types: [synchronize]
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||
steps:
|
||||
- name: Apply label
|
||||
uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||
@@ -1,30 +0,0 @@
|
||||
name: Stale PR Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 */12 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
prs-stale-conflicts:
|
||||
name: Check PRs with merge conflicts
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
ascending: true
|
||||
operations-per-run: 150
|
||||
# The merge conflict action will remove the label when updated
|
||||
remove-stale-when-updated: false
|
||||
days-before-stale: -1
|
||||
days-before-close: 90
|
||||
days-before-issue-close: -1
|
||||
stale-pr-label: merge conflict
|
||||
close-pr-message: |-
|
||||
This PR has been closed due to having unresolved merge conflicts.
|
||||
@@ -1,82 +0,0 @@
|
||||
name: '🆙 Auto bump_version'
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
TAG_BRANCH:
|
||||
required: true
|
||||
description: release-x.y.z
|
||||
NEXT_VERSION:
|
||||
required: true
|
||||
description: x.y.z
|
||||
|
||||
jobs:
|
||||
auto_bump_version:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'release' && !contains(github.event.release.tag_name, 'rc') }}
|
||||
env:
|
||||
TAG_BRANCH: ${{ github.event.release.target_commitish }}
|
||||
steps:
|
||||
- name: Wait for deploy checks to finish
|
||||
uses: jitterbit/await-check-suites@292a541bb7618078395b2ce711a0d89cfb8a568a # v1
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
intervalSeconds: 60
|
||||
timeoutSeconds: 3600
|
||||
|
||||
- name: Setup YQ
|
||||
uses: chrisdickinson/setup-yq@fa3192edd79d6eb0e4e12de8dde3a0c26f2b853b # latest
|
||||
with:
|
||||
yq-version: v4.9.8
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
- name: Setup EnvVars
|
||||
run: |-
|
||||
CURRENT_VERSION=$(yq e '.version' build.yaml)
|
||||
CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*}
|
||||
CURRENT_PATCH=${CURRENT_VERSION##*.}
|
||||
echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV
|
||||
echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV
|
||||
echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV
|
||||
echo "NEXT_VERSION=${CURRENT_MAJOR_MINOR}.$(($CURRENT_PATCH + 1))" >> $GITHUB_ENV
|
||||
|
||||
- name: Run bump_version
|
||||
run: ./bump_version ${{ env.NEXT_VERSION }}
|
||||
|
||||
- name: Commit Changes
|
||||
run: |-
|
||||
git config user.name "jellyfin-bot"
|
||||
git config user.email "team@jellyfin.org"
|
||||
git checkout ${{ env.TAG_BRANCH }}
|
||||
git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
|
||||
git push origin ${{ env.TAG_BRANCH }}
|
||||
|
||||
manual_bump_version:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
env:
|
||||
TAG_BRANCH: ${{ github.event.inputs.TAG_BRANCH }}
|
||||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
- name: Run bump_version
|
||||
run: ./bump_version ${{ env.NEXT_VERSION }}
|
||||
|
||||
- name: Commit Changes
|
||||
run: |-
|
||||
git config user.name "jellyfin-bot"
|
||||
git config user.email "team@jellyfin.org"
|
||||
git checkout ${{ env.TAG_BRANCH }}
|
||||
git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
|
||||
git push origin ${{ env.TAG_BRANCH }}
|
||||
@@ -0,0 +1,79 @@
|
||||
when:
|
||||
- event: pull_request
|
||||
- event: push
|
||||
|
||||
steps:
|
||||
# Restore, build and test in one step so the NuGet cache stays on the step's
|
||||
# own ephemeral storage instead of the 10Gi workspace volume.
|
||||
# global.json pins the .NET 10 SDK (10.0.0, rollForward latestMinor).
|
||||
- name: build-test
|
||||
image: mcr.microsoft.com/dotnet/sdk:10.0
|
||||
environment:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
|
||||
DOTNET_NOLOGO: "1"
|
||||
commands:
|
||||
- dotnet --info
|
||||
- dotnet restore Jellyfin.sln
|
||||
- dotnet build Jellyfin.sln -c Release --no-restore
|
||||
# libSkiaSharp needs fontconfig to load and the SDK image does not ship it.
|
||||
# The mirror occasionally serves a half-synced index, so update is retried.
|
||||
- apt-get -o Acquire::Retries=3 update || apt-get -o Acquire::Retries=3 update
|
||||
- apt-get install -y --no-install-recommends libfontconfig1
|
||||
- ldconfig -p | grep -q libfontconfig
|
||||
# BackupServiceTests is excluded because BackupService refuses to write a
|
||||
# backup with less than 5GiB free on its target path. The clone and build
|
||||
# output fill 5.7G of the 9.8G workspace volume, leaving 4.1G. Raising the
|
||||
# agent's workspace volume size is the only way to run it here.
|
||||
- df -h .
|
||||
- dotnet test Jellyfin.sln -c Release --no-build --verbosity minimal --filter "Category!=RequiresDocker&FullyQualifiedName!~Integration&FullyQualifiedName!~BackupServiceTests"
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: jellyfin-ha-src
|
||||
resources:
|
||||
requests:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
ephemeral-storage: 10Gi
|
||||
limits:
|
||||
memory: 8Gi
|
||||
cpu: 4
|
||||
ephemeral-storage: 20Gi
|
||||
|
||||
# The PostgreSQL migration tests are the only ones that run the startup migration chain against
|
||||
# the provider production uses, and the filter above has always skipped them.
|
||||
# The server runs inside this step: the kubernetes backend has no docker daemon for
|
||||
# testcontainers, and a postgres service container deadlocks the step because the backend mounts
|
||||
# the ReadWriteOnce workspace volume into service pods and schedules them on another node.
|
||||
# Its data directory lives on the step's ephemeral storage, not on the workspace volume.
|
||||
# Scoped to Jellyfin.Server.Tests: the three classes in Jellyfin.Database.Tests.PostgreSQL still
|
||||
# start their own container, and PostgreSqlProviderTests already fails on main - on an EF 10
|
||||
# scalar query and on its own data - which a third test in the class then inherits.
|
||||
- name: postgres-migration-chain
|
||||
image: mcr.microsoft.com/dotnet/sdk:10.0
|
||||
depends_on:
|
||||
- build-test
|
||||
environment:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
|
||||
DOTNET_NOLOGO: "1"
|
||||
JELLYFIN_TEST_POSTGRES: "Host=127.0.0.1;Port=5432;Database=postgres;Username=postgres"
|
||||
commands:
|
||||
- apt-get -o Acquire::Retries=3 update || apt-get -o Acquire::Retries=3 update
|
||||
- DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends postgresql
|
||||
- install -d -o postgres -g postgres /tmp/pgdata /tmp/pgrun
|
||||
- PGBIN=$(ls -d /usr/lib/postgresql/*/bin | tail -1)
|
||||
- su postgres -c "$PGBIN/initdb -D /tmp/pgdata -A trust -U postgres"
|
||||
- su postgres -c "$PGBIN/pg_ctl -D /tmp/pgdata -o \"-c listen_addresses=127.0.0.1 -k /tmp/pgrun\" -l /tmp/pg.log -w start"
|
||||
- dotnet build tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj -c Release
|
||||
- dotnet test tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj -c Release --no-build --verbosity minimal --filter "Category=RequiresDocker"
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: jellyfin-ha-src
|
||||
resources:
|
||||
requests:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
ephemeral-storage: 6Gi
|
||||
limits:
|
||||
memory: 6Gi
|
||||
cpu: 4
|
||||
ephemeral-storage: 12Gi
|
||||
+9
-1
@@ -143,6 +143,7 @@
|
||||
- [Smith00101010](https://github.com/Smith00101010)
|
||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
|
||||
- [st7105](https://github.com/st7105)
|
||||
- [sparky8251](https://github.com/sparky8251)
|
||||
- [spookbits](https://github.com/spookbits)
|
||||
- [ssenart](https://github.com/ssenart)
|
||||
@@ -172,6 +173,7 @@
|
||||
- [whooo](https://github.com/whooo)
|
||||
- [WiiPlayer2](https://github.com/WiiPlayer2)
|
||||
- [WillWill56](https://github.com/WillWill56)
|
||||
- [WizardOfYendor1](https://github.com/WizardOfYendor1)
|
||||
- [wtayl0r](https://github.com/wtayl0r)
|
||||
- [Wuerfelbecher](https://github.com/Wuerfelbecher)
|
||||
- [Wunax](https://github.com/Wunax)
|
||||
@@ -233,6 +235,12 @@
|
||||
- [MSalman5230](https://github.com/MSalman5230)
|
||||
- [dwandw](https://github.com/dwandw)
|
||||
- [Lampan-git](https://github.com/Lampan-git)
|
||||
- [elio42](https://github.com/elio42)
|
||||
- [rwebster85](https://github.com/rwebster85)
|
||||
- [Florin-Popescu](https://github.com/Florin-Popescu)
|
||||
- [m0g3r](https://github.com/m0g3r)
|
||||
- [martin-77](https://github.com/martin-77)
|
||||
- [Oggeb1](https://github.com/Oggeb1)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
@@ -243,7 +251,7 @@
|
||||
- [Mark2xv](https://github.com/Mark2xv)
|
||||
- [ScottRapsey](https://github.com/ScottRapsey)
|
||||
- [skynet600](https://github.com/skynet600)
|
||||
- [Cheesegeezer](https://githum.com/Cheesegeezer)
|
||||
- [Cheesegeezer](https://github.com/Cheesegeezer)
|
||||
- [Radeon](https://github.com/radeonorama)
|
||||
- [gcw07](https://github.com/gcw07)
|
||||
- [SivaramAdhiappan](https://github.com/shivaram1190)
|
||||
|
||||
+34
-27
@@ -8,8 +8,9 @@
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture.Xunit3" Version="4.19.0" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||
<PackageVersion Include="AWSSDK.S3" Version="4.0.103.1" />
|
||||
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
||||
<PackageVersion Include="BitFaster.Caching" Version="2.6.0" />
|
||||
<PackageVersion Include="BitFaster.Caching" Version="2.6.1" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
@@ -17,7 +18,7 @@
|
||||
<PackageVersion Include="Diacritics" Version="4.1.8" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit.v3" Version="3.3.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit.v3" Version="3.3.4" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.5" />
|
||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||
@@ -26,33 +27,36 @@
|
||||
<PackageVersion Include="libse" Version="4.0.12" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.6.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.10" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.9.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.9.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.11" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="NEbml" Version="1.1.0.5" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageVersion Include="Npgsql" Version="10.0.3" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||
<PackageVersion Include="PDFtoImage" Version="5.2.1" />
|
||||
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
@@ -68,21 +72,24 @@
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpCompress" Version="0.50.0" />
|
||||
<PackageVersion Include="SharpCompress" Version="0.50.4" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.3.0" />
|
||||
<PackageVersion Include="SkiaSharp" Version="3.119.4" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.119.4" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StackExchange.Redis" Version="2.13.17" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="3.7.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.2.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.11" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.15.3" />
|
||||
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.15.0" />
|
||||
<PackageVersion Include="Testcontainers.Redis" Version="4.15.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.16.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="3.0.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.7.0" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
||||
<PackageVersion Include="Xunit.v3.Priority" Version="1.1.18" />
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# ── Build stage ──────────────────────────────────────────────────────────────
|
||||
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Restore dependencies first (layer-cache friendly)
|
||||
COPY ["Jellyfin.sln", "global.json", "nuget.config", "Directory.Build.props", "Directory.Packages.props", "./"]
|
||||
COPY ["SharedVersion.cs", "BannedSymbols.txt", "stylecop.json", "./"]
|
||||
|
||||
# Copy all project files so dotnet restore can resolve the full dependency graph
|
||||
COPY Emby.Naming/ Emby.Naming/
|
||||
COPY Emby.Photos/ Emby.Photos/
|
||||
COPY Emby.Server.Implementations/ Emby.Server.Implementations/
|
||||
COPY Jellyfin.Api/ Jellyfin.Api/
|
||||
COPY Jellyfin.Data/ Jellyfin.Data/
|
||||
COPY Jellyfin.Server/ Jellyfin.Server/
|
||||
COPY Jellyfin.Server.Implementations/ Jellyfin.Server.Implementations/
|
||||
COPY MediaBrowser.Common/ MediaBrowser.Common/
|
||||
COPY MediaBrowser.Controller/ MediaBrowser.Controller/
|
||||
COPY MediaBrowser.LocalMetadata/ MediaBrowser.LocalMetadata/
|
||||
COPY MediaBrowser.MediaEncoding/ MediaBrowser.MediaEncoding/
|
||||
COPY MediaBrowser.Model/ MediaBrowser.Model/
|
||||
COPY MediaBrowser.Providers/ MediaBrowser.Providers/
|
||||
COPY MediaBrowser.XbmcMetadata/ MediaBrowser.XbmcMetadata/
|
||||
COPY src/ src/
|
||||
|
||||
RUN dotnet restore Jellyfin.Server/Jellyfin.Server.csproj \
|
||||
--runtime linux-x64
|
||||
|
||||
# Publish the server (and all transitive dependencies, including the
|
||||
# PostgreSQL provider assembly added by this fork).
|
||||
# Note: TreatWarningsAsErrors is disabled for the Docker build — StyleCop
|
||||
# analyzer violations in upstream src/ projects would otherwise block the
|
||||
# image build. StyleCop is enforced in the CI pipeline, not the Dockerfile.
|
||||
RUN dotnet publish Jellyfin.Server/Jellyfin.Server.csproj \
|
||||
--configuration Release \
|
||||
--runtime linux-x64 \
|
||||
--self-contained false \
|
||||
--no-restore \
|
||||
-p:TreatWarningsAsErrors=false \
|
||||
--output /app
|
||||
|
||||
# ── Web client stage ──────────────────────────────────────────────────────────
|
||||
# Install jellyfin-web via the official Jellyfin apt repo.
|
||||
# Package suffix in the bookworm repo is +deb12 (e.g. 12.0+deb12).
|
||||
# Web assets land at /usr/share/jellyfin/web/ — stable, prebuilt, no npm required.
|
||||
# The web client version must match the server version this fork is based on.
|
||||
FROM --platform=linux/amd64 debian:bookworm-slim AS webclient
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl gnupg ca-certificates \
|
||||
&& curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key \
|
||||
| gpg --dearmor -o /usr/share/keyrings/jellyfin.gpg \
|
||||
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/jellyfin.gpg] https://repo.jellyfin.org/debian bookworm main" \
|
||||
> /etc/apt/sources.list.d/jellyfin.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends "jellyfin-web=12.0+deb12" \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& echo "Web client files:" && ls /usr/share/jellyfin/web/ | head -10
|
||||
|
||||
# ── Runtime stage ─────────────────────────────────────────────────────────────
|
||||
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
|
||||
# Install FFmpeg and native dependencies required by SkiaSharp and fontconfig.
|
||||
# libicu, libssl, and liblttng-ust are already present in the dotnet/aspnet base image.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ffmpeg \
|
||||
fontconfig \
|
||||
libfontconfig1 \
|
||||
libfreetype6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /jellyfin
|
||||
|
||||
COPY --from=build /app .
|
||||
COPY --from=webclient /usr/share/jellyfin/web ./jellyfin-web/
|
||||
|
||||
# Jellyfin default ports
|
||||
EXPOSE 8096
|
||||
EXPOSE 8920
|
||||
|
||||
# Data / config volumes
|
||||
VOLUME ["/config", "/cache", "/media"]
|
||||
|
||||
ENV JELLYFIN_DATA_DIR=/config \
|
||||
JELLYFIN_CACHE_DIR=/cache \
|
||||
JELLYFIN_LOG_DIR=/config/log \
|
||||
JELLYFIN_CONFIG_DIR=/config
|
||||
|
||||
ENTRYPOINT ["./jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
"--webdir", "/jellyfin/jellyfin-web"]
|
||||
@@ -0,0 +1,55 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Runtime-only image — the .NET publish step runs on the CI host (runner),
|
||||
# not inside this Dockerfile.
|
||||
|
||||
# ── Web client stage ──────────────────────────────────────────────────────────
|
||||
# Install jellyfin-web via the official Jellyfin apt repo.
|
||||
# Package suffix in the bookworm repo is +deb12 (e.g. 12.0+deb12).
|
||||
# Web assets land at /usr/share/jellyfin/web/ — stable, prebuilt, no npm required.
|
||||
# The web client version must match the server version this fork is based on.
|
||||
FROM --platform=linux/amd64 debian:bookworm-slim AS webclient
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl gnupg ca-certificates \
|
||||
&& curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key \
|
||||
| gpg --dearmor -o /usr/share/keyrings/jellyfin.gpg \
|
||||
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/jellyfin.gpg] https://repo.jellyfin.org/debian bookworm main" \
|
||||
> /etc/apt/sources.list.d/jellyfin.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends "jellyfin-web=12.0+deb12" \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ── Runtime stage ─────────────────────────────────────────────────────────────
|
||||
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
|
||||
# Install FFmpeg and native dependencies required by SkiaSharp and fontconfig.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ffmpeg \
|
||||
fontconfig \
|
||||
libfontconfig1 \
|
||||
libfreetype6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /jellyfin
|
||||
|
||||
# Copy the pre-built publish output produced by `dotnet publish` on the CI host.
|
||||
COPY publish-output/ .
|
||||
# Copy the jellyfin-web client assets from the webclient stage.
|
||||
COPY --from=webclient /usr/share/jellyfin/web ./jellyfin-web/
|
||||
|
||||
# Jellyfin default ports
|
||||
EXPOSE 8096
|
||||
EXPOSE 8920
|
||||
|
||||
# Data / config volumes
|
||||
VOLUME ["/config", "/cache", "/media"]
|
||||
|
||||
ENV JELLYFIN_DATA_DIR=/config \
|
||||
JELLYFIN_CACHE_DIR=/cache \
|
||||
JELLYFIN_LOG_DIR=/config/log
|
||||
|
||||
ENTRYPOINT ["./jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
"--webdir", "/jellyfin/jellyfin-web"]
|
||||
@@ -57,7 +57,6 @@ namespace Emby.Naming.Common
|
||||
".nrg",
|
||||
".nsv",
|
||||
".nuv",
|
||||
".ogg",
|
||||
".ogm",
|
||||
".ogv",
|
||||
".pva",
|
||||
@@ -152,7 +151,7 @@ namespace Emby.Naming.Common
|
||||
|
||||
CleanStrings =
|
||||
[
|
||||
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS)(?=[ _\,\.\(\)\[\]\-]|$)",
|
||||
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|web-dl|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS)(?=[ _\,\.\(\)\[\]\-]|$)",
|
||||
@"^\s*(?<cleaned>.+?)((\s*\[[^\]]+\]\s*)+)(\.[^\s]+)?$",
|
||||
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
||||
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
||||
@@ -362,7 +361,10 @@ namespace Emby.Naming.Common
|
||||
// Not a Kodi rule as well, but the expression below also causes false positives,
|
||||
// so we make sure this one gets tested first.
|
||||
// "Foo Bar 889"
|
||||
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
|
||||
// Names carrying an SxxEyy marker are excluded because the Kodi expression above already covers them.
|
||||
// Without that guard this expression reads digits out of the title instead, turning
|
||||
// "S01E01 1-23-45 [Bluray-1080p]" into episodes 1 through 45.
|
||||
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?![^\\\/]*[Ss][0-9]+[][ ._-]*[Ee][0-9]+)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
@@ -44,7 +44,14 @@ namespace Emby.Naming.ExternalFiles
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
// .idx carries VobSub per-track language metadata. Recognize it here rather
|
||||
// than adding it to NamingOptions.SubtitleFileExtensions, which also gates
|
||||
// subtitle uploads/saves.
|
||||
var isVobSubIndex = _type == DlnaProfileType.Subtitle && extension.Equals(".idx", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!isVobSubIndex
|
||||
&& !(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
&& !(_type == DlnaProfileType.Lyric && _namingOptions.LyricFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
|
||||
@@ -158,7 +158,9 @@ namespace Emby.Naming.TV
|
||||
if (nextIndex >= name.Length
|
||||
|| !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
|
||||
{
|
||||
if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
|
||||
// A range cannot end before it starts, so a lower number belongs to the episode title rather than to a range.
|
||||
if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)
|
||||
&& num >= result.EpisodeNumber)
|
||||
{
|
||||
result.EndingEpisodeNumber = num;
|
||||
}
|
||||
@@ -226,7 +228,7 @@ namespace Emby.Naming.TV
|
||||
info.SeriesName = result.SeriesName;
|
||||
}
|
||||
|
||||
if (!info.EndingEpisodeNumber.HasValue && info.EpisodeNumber.HasValue)
|
||||
if (!info.EndingEpisodeNumber.HasValue && result.EndingEpisodeNumber >= info.EpisodeNumber)
|
||||
{
|
||||
info.EndingEpisodeNumber = result.EndingEpisodeNumber;
|
||||
}
|
||||
|
||||
@@ -52,13 +52,18 @@ namespace Emby.Naming.Video
|
||||
while (path.Length > 0)
|
||||
{
|
||||
var index = path.IndexOfAny(delimiters);
|
||||
ReadOnlySpan<char> currentSlice;
|
||||
if (index == -1)
|
||||
{
|
||||
index = path.Length - 1;
|
||||
// No delimiter left, the last token is the remainder of the path
|
||||
currentSlice = path;
|
||||
path = default;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentSlice = path[..index];
|
||||
path = path[(index + 1)..];
|
||||
}
|
||||
|
||||
var currentSlice = path[..index];
|
||||
path = path[(index + 1)..];
|
||||
|
||||
if (!foundPrefix)
|
||||
{
|
||||
|
||||
@@ -987,8 +987,9 @@ namespace Emby.Server.Implementations
|
||||
/// <inheritdoc/>
|
||||
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
|
||||
{
|
||||
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
||||
if (hostname.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
// If the smartAPI isn't already a complete URL then treat it as a host or ip.
|
||||
if (hostname.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
|| hostname.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return hostname.TrimEnd('/');
|
||||
}
|
||||
|
||||
@@ -107,7 +107,8 @@ namespace Emby.Server.Implementations.Collections
|
||||
SaveLocalMetadata = true
|
||||
};
|
||||
|
||||
var name = _localizationManager.GetLocalizedString("Collections");
|
||||
// This names a library for the whole server, so ignore the requesting client's language.
|
||||
var name = _localizationManager.GetServerLocalizedString("Collections");
|
||||
|
||||
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -185,6 +185,13 @@ namespace Emby.Server.Implementations.Dto
|
||||
allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
|
||||
}
|
||||
|
||||
// Batch-fetch by-name item counts to avoid N+1 queries
|
||||
Dictionary<Guid, ItemCounts>? itemCountsBatch = null;
|
||||
if (options.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
itemCountsBatch = GetItemCountsBatch(accessibleItems, user);
|
||||
}
|
||||
|
||||
// Batch-fetch child counts for all folders to avoid N+1 queries
|
||||
Dictionary<Guid, int>? childCountBatch = null;
|
||||
if (options.ContainsField(ItemFields.ChildCount))
|
||||
@@ -192,7 +199,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
var folderIds = accessibleItems.OfType<Folder>().Select(f => f.Id).ToList();
|
||||
if (folderIds.Count > 0)
|
||||
{
|
||||
childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user?.Id);
|
||||
childCountBatch = _libraryManager.GetChildCountBatch(folderIds, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +249,29 @@ namespace Emby.Server.Implementations.Dto
|
||||
artistsBatch = _libraryManager.GetArtists(artistNames.ToArray());
|
||||
}
|
||||
|
||||
// Batch-fetch people across all items to avoid one GetPeople query per item.
|
||||
IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null;
|
||||
if (options.ContainsField(ItemFields.People))
|
||||
{
|
||||
var peopleItemIds = accessibleItems.Where(i => i.SupportsPeople).Select(i => i.Id).ToList();
|
||||
if (peopleItemIds.Count > 0)
|
||||
{
|
||||
peopleBatch = _libraryManager.GetPeopleByItems(peopleItemIds);
|
||||
}
|
||||
}
|
||||
|
||||
// Batch-detect which videos own alternate versions to avoid the per-item alternate-version
|
||||
// queries in MediaSourceCount. Videos absent from this set have a single media source.
|
||||
IReadOnlySet<Guid>? alternateVersionItemIds = null;
|
||||
if (options.ContainsField(ItemFields.MediaSourceCount))
|
||||
{
|
||||
var versionItemIds = accessibleItems.OfType<Video>().Select(i => i.Id).ToList();
|
||||
if (versionItemIds.Count > 0)
|
||||
{
|
||||
alternateVersionItemIds = _libraryManager.GetItemIdsWithAlternateVersions(versionItemIds);
|
||||
}
|
||||
}
|
||||
|
||||
for (int index = 0; index < accessibleItems.Count; index++)
|
||||
{
|
||||
var item = accessibleItems[index];
|
||||
@@ -255,7 +285,9 @@ namespace Emby.Server.Implementations.Dto
|
||||
childCountBatch,
|
||||
playedCountBatch,
|
||||
artistsBatch,
|
||||
resumeDataBatch?.GetValueOrDefault(item.Id));
|
||||
resumeDataBatch?.GetValueOrDefault(item.Id),
|
||||
peopleBatch,
|
||||
alternateVersionItemIds);
|
||||
|
||||
if (item is LiveTvChannel tvChannel)
|
||||
{
|
||||
@@ -268,7 +300,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
SetItemByNameInfo(dto, user);
|
||||
SetItemByNameInfo(dto, user, itemCountsBatch);
|
||||
}
|
||||
|
||||
returnItems[index] = dto;
|
||||
@@ -317,7 +349,9 @@ namespace Emby.Server.Implementations.Dto
|
||||
Dictionary<Guid, int>? childCountBatch = null,
|
||||
Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null,
|
||||
IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null,
|
||||
VersionResumeData? resumeData = null)
|
||||
VersionResumeData? resumeData = null,
|
||||
IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null,
|
||||
IReadOnlySet<Guid>? alternateVersionItemIds = null)
|
||||
{
|
||||
var dto = new BaseItemDto
|
||||
{
|
||||
@@ -331,7 +365,15 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.People))
|
||||
{
|
||||
AttachPeople(dto, item, user);
|
||||
IReadOnlyList<PersonInfo>? prefetchedPeople = null;
|
||||
if (peopleBatch is not null)
|
||||
{
|
||||
// The batch omits items with no people, so a miss means "no people",
|
||||
// not "not fetched". Use an empty list to skip the per-item query.
|
||||
prefetchedPeople = peopleBatch.GetValueOrDefault(item.Id) ?? [];
|
||||
}
|
||||
|
||||
AttachPeople(dto, item, user, prefetchedPeople);
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
|
||||
@@ -378,7 +420,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
AttachStudios(dto, item);
|
||||
}
|
||||
|
||||
AttachBasicFields(dto, item, owner, options, artistsBatch, user);
|
||||
AttachBasicFields(dto, item, owner, options, artistsBatch, user, alternateVersionItemIds);
|
||||
|
||||
if (options.ContainsField(ItemFields.CanDelete))
|
||||
{
|
||||
@@ -483,14 +525,36 @@ namespace Emby.Server.Implementations.Dto
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void SetItemByNameInfo(BaseItemDto dto, User? user)
|
||||
private Dictionary<Guid, ItemCounts> GetItemCountsBatch(IReadOnlyList<BaseItem> items, User? user)
|
||||
{
|
||||
var result = new Dictionary<Guid, ItemCounts>();
|
||||
|
||||
foreach (var group in items.GroupBy(item => item.GetBaseItemKind()))
|
||||
{
|
||||
if (!_relatedItemKinds.TryGetValue(group.Key, out var relatedItemKinds))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var ids = group.Select(item => item.Id).ToArray();
|
||||
foreach (var (id, counts) in _libraryManager.GetItemCountsForNameItems(group.Key, ids, relatedItemKinds, user))
|
||||
{
|
||||
result[id] = counts;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SetItemByNameInfo(BaseItemDto dto, User? user, IReadOnlyDictionary<Guid, ItemCounts>? prefetchedCounts = null)
|
||||
{
|
||||
if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var counts = _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
|
||||
var counts = prefetchedCounts?.GetValueOrDefault(dto.Id)
|
||||
?? _libraryManager.GetItemCountsForNameItem(dto.Type, dto.Id, relatedItemKinds, user);
|
||||
|
||||
dto.AlbumCount = counts.AlbumCount;
|
||||
dto.ArtistCount = counts.ArtistCount;
|
||||
@@ -576,7 +640,11 @@ namespace Emby.Server.Implementations.Dto
|
||||
// For these types we can try to optimize and assume these values will be equal
|
||||
if (item is MusicAlbum || item is Season || item is Playlist)
|
||||
{
|
||||
dto.ChildCount = dto.RecursiveItemCount;
|
||||
if (dto.RecursiveItemCount > 0)
|
||||
{
|
||||
dto.ChildCount = dto.RecursiveItemCount;
|
||||
}
|
||||
|
||||
var folderChildCount = folder.LinkedChildren.Length;
|
||||
// The default is an empty array, so we can't reliably use the count when it's empty
|
||||
if (folderChildCount > 0)
|
||||
@@ -661,7 +729,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
return count;
|
||||
}
|
||||
|
||||
// Fall back to individual query for special cases (Series, Season, etc.)
|
||||
// Only reached when no batch was computed: the batch holds an entry for every folder it
|
||||
// was asked about, zero included.
|
||||
return folder.GetChildCount(user);
|
||||
}
|
||||
|
||||
@@ -742,12 +811,18 @@ namespace Emby.Server.Implementations.Dto
|
||||
/// <param name="dto">The dto.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="user">The requesting user.</param>
|
||||
private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null)
|
||||
/// <param name="prefetchedPeople">People fetched in batch by the caller; when null the people are queried per item.</param>
|
||||
private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null, IReadOnlyList<PersonInfo>? prefetchedPeople = null)
|
||||
{
|
||||
// When rendering a page of items the caller batch-fetches people for every item up
|
||||
// front and passes them in, avoiding one GetPeople query per item. Fall back to the
|
||||
// per-item query for the single item path where no batch is available.
|
||||
var source = prefetchedPeople ?? _libraryManager.GetPeople(item);
|
||||
|
||||
// Ordering by person type to ensure actors and artists are at the front.
|
||||
// This is taking advantage of the fact that they both begin with A
|
||||
// This should be improved in the future
|
||||
var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
|
||||
var people = source.OrderBy(i => i.SortOrder ?? int.MaxValue)
|
||||
.ThenBy(i =>
|
||||
{
|
||||
if (i.IsType(PersonKind.Actor))
|
||||
@@ -957,7 +1032,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="artistsBatch">Optional pre-fetched artist lookup shared across a batch of items.</param>
|
||||
/// <param name="user">The user, for per-user values such as the accessible media source count.</param>
|
||||
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null, User? user = null)
|
||||
/// <param name="alternateVersionItemIds">Optional pre-fetched set of item IDs that own alternate versions, shared across a batch of items.</param>
|
||||
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null, User? user = null, IReadOnlySet<Guid>? alternateVersionItemIds = null)
|
||||
{
|
||||
if (options.ContainsField(ItemFields.DateCreated))
|
||||
{
|
||||
@@ -1088,7 +1164,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.ParentId = item.DisplayParentId;
|
||||
}
|
||||
|
||||
AddInheritedImages(dto, item, options, owner);
|
||||
AddInheritedImages(dto, item, options, owner, artistsBatch);
|
||||
|
||||
if (options.ContainsField(ItemFields.Path))
|
||||
{
|
||||
@@ -1271,15 +1347,27 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.MediaSourceCount))
|
||||
{
|
||||
// Match the per-user filtering of the media sources: versions the user cannot
|
||||
// access are not selectable, so they must not count towards the badge either.
|
||||
var mediaSourceCount = user is null
|
||||
|| (!video.PrimaryVersionId.HasValue && video.LinkedAlternateVersions.Length == 0 && !video.HasLocalAlternateVersions)
|
||||
? video.MediaSourceCount
|
||||
: video.GetAllVersions().Count(v => v.Id.Equals(video.Id) || v.IsVisibleStandalone(user));
|
||||
if (mediaSourceCount != 1)
|
||||
// A video with no primary version and no alternate versions always has a single
|
||||
// media source. Only compute the count for videos that might have more: a primary
|
||||
// version, or membership in the batch's set of items that own alternate versions.
|
||||
// Without the batch we can't rule it out, so fall back to computing (the single-item
|
||||
// path). Everything else is the common case and keeps the default count of one.
|
||||
var mayHaveAlternateVersions = alternateVersionItemIds is null
|
||||
|| video.PrimaryVersionId.HasValue
|
||||
|| alternateVersionItemIds.Contains(video.Id);
|
||||
|
||||
if (mayHaveAlternateVersions)
|
||||
{
|
||||
dto.MediaSourceCount = mediaSourceCount;
|
||||
// Match the per-user filtering of the media sources: versions the user cannot
|
||||
// access are not selectable, so they must not count towards the badge either.
|
||||
var mediaSourceCount = user is null
|
||||
|| (!video.PrimaryVersionId.HasValue && video.LinkedAlternateVersions.Length == 0 && !video.HasLocalAlternateVersions)
|
||||
? video.MediaSourceCount
|
||||
: video.GetAllVersions().Count(v => v.Id.Equals(video.Id) || v.IsVisibleStandalone(user));
|
||||
if (mediaSourceCount != 1)
|
||||
{
|
||||
dto.MediaSourceCount = mediaSourceCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1519,11 +1607,11 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
}
|
||||
|
||||
private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem)
|
||||
private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch)
|
||||
{
|
||||
if (currentItem is MusicAlbum musicAlbum)
|
||||
{
|
||||
var artist = musicAlbum.GetMusicArtist(new DtoOptions(false));
|
||||
var artist = GetBatchedAlbumArtist(musicAlbum, artistsBatch) ?? musicAlbum.GetMusicArtist(new DtoOptions(false));
|
||||
if (artist is not null)
|
||||
{
|
||||
return artist;
|
||||
@@ -1540,7 +1628,20 @@ namespace Emby.Server.Implementations.Dto
|
||||
return parent;
|
||||
}
|
||||
|
||||
private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner)
|
||||
private static MusicArtist? GetBatchedAlbumArtist(MusicAlbum album, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch)
|
||||
{
|
||||
if (artistsBatch is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = album.AlbumArtists.Count > 0 ? album.AlbumArtists[0] : null;
|
||||
return !string.IsNullOrEmpty(name) && artistsBatch.TryGetValue(name, out var artists) && artists.Length > 0
|
||||
? artists[0]
|
||||
: null;
|
||||
}
|
||||
|
||||
private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch)
|
||||
{
|
||||
if (item is UserView { ViewType: CollectionType.playlists } playlistsView
|
||||
&& options.GetImageLimit(ImageType.Primary) > 0
|
||||
@@ -1585,7 +1686,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|| (!(imageTags is not null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
|
||||
|| parent is Series)
|
||||
{
|
||||
parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
|
||||
parent ??= isFirst ? GetImageDisplayParent(item, item, artistsBatch) ?? owner : parent;
|
||||
if (parent is null)
|
||||
{
|
||||
break;
|
||||
@@ -1644,7 +1745,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
break;
|
||||
}
|
||||
|
||||
parent = GetImageDisplayParent(parent, item);
|
||||
parent = GetImageDisplayParent(parent, item, artistsBatch);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Ignore" />
|
||||
<PackageReference Include="StackExchange.Redis" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -27,6 +27,11 @@ namespace Emby.Server.Implementations.EntryPoints;
|
||||
/// </summary>
|
||||
public sealed class LibraryChangedNotifier : IHostedService, IDisposable
|
||||
{
|
||||
// A batch holds a live reference to every item it names, so it has to stay small enough that a
|
||||
// library scan - which changes items faster than any batch window closes - cannot grow it without
|
||||
// bound. Reached only by a scan; interactive use closes a batch on the window long before this.
|
||||
internal const int MaxBatchSize = 2000;
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
@@ -35,11 +40,11 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
|
||||
private readonly ILogger<LibraryChangedNotifier> _logger;
|
||||
|
||||
private readonly Lock _libraryChangedSyncLock = new();
|
||||
private readonly List<Folder> _foldersAddedTo = new();
|
||||
private readonly List<Folder> _foldersRemovedFrom = new();
|
||||
private readonly List<BaseItem> _itemsAdded = new();
|
||||
private readonly List<BaseItem> _itemsRemoved = new();
|
||||
private readonly List<BaseItem> _itemsUpdated = new();
|
||||
private readonly Dictionary<Guid, Folder> _foldersAddedTo = [];
|
||||
private readonly Dictionary<Guid, Folder> _foldersRemovedFrom = [];
|
||||
private readonly Dictionary<Guid, BaseItem> _itemsAdded = [];
|
||||
private readonly Dictionary<Guid, BaseItem> _itemsRemoved = [];
|
||||
private readonly Dictionary<Guid, BaseItem> _itemsUpdated = [];
|
||||
private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new();
|
||||
|
||||
private Timer? _libraryUpdateTimer;
|
||||
@@ -173,7 +178,7 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
|
||||
private void OnLibraryItemRemoved(object? sender, ItemChangeEventArgs e)
|
||||
=> OnLibraryChange(e.Item, e.Parent, _itemsRemoved, _foldersRemovedFrom);
|
||||
|
||||
private void OnLibraryChange(BaseItem item, BaseItem parent, List<BaseItem> itemsList, List<Folder>? foldersList)
|
||||
private void OnLibraryChange(BaseItem item, BaseItem parent, Dictionary<Guid, BaseItem> itemsList, Dictionary<Guid, Folder>? foldersList)
|
||||
{
|
||||
if (!FilterItem(item))
|
||||
{
|
||||
@@ -182,23 +187,28 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
|
||||
|
||||
lock (_libraryChangedSyncLock)
|
||||
{
|
||||
var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration);
|
||||
|
||||
// The window runs from the first change of a batch and is never extended. Extending it on
|
||||
// every change would keep a library scan's batch open for the whole scan, and the batch
|
||||
// holds the items it names alive, so it would grow to the size of the library.
|
||||
if (_libraryUpdateTimer is null)
|
||||
{
|
||||
var updateDuration = TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration);
|
||||
_libraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, updateDuration, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
_libraryUpdateTimer.Change(updateDuration, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
if (foldersList is not null && parent is Folder folder)
|
||||
{
|
||||
foldersList.Add(folder);
|
||||
foldersList[folder.Id] = folder;
|
||||
}
|
||||
|
||||
itemsList.Add(item);
|
||||
itemsList[item.Id] = item;
|
||||
|
||||
// A window long enough to cover a burst still has to give way once the batch is large
|
||||
// enough to be worth sending on its own.
|
||||
if (_itemsAdded.Count + _itemsRemoved.Count + _itemsUpdated.Count >= MaxBatchSize)
|
||||
{
|
||||
_libraryUpdateTimer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,22 +221,16 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
|
||||
List<BaseItem> itemsRemoved;
|
||||
lock (_libraryChangedSyncLock)
|
||||
{
|
||||
// Remove dupes in case some were saved multiple times
|
||||
foldersAddedTo = _foldersAddedTo
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
foldersRemovedFrom = _foldersRemovedFrom
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
foldersAddedTo = _foldersAddedTo.Values.ToList();
|
||||
foldersRemovedFrom = _foldersRemovedFrom.Values.ToList();
|
||||
|
||||
itemsUpdated = _itemsUpdated
|
||||
.Where(i => !_itemsAdded.Contains(i))
|
||||
.DistinctBy(x => x.Id)
|
||||
.Where(e => !_itemsAdded.ContainsKey(e.Key))
|
||||
.Select(e => e.Value)
|
||||
.ToList();
|
||||
|
||||
itemsAdded = _itemsAdded.ToList();
|
||||
itemsRemoved = _itemsRemoved.ToList();
|
||||
itemsAdded = _itemsAdded.Values.ToList();
|
||||
itemsRemoved = _itemsRemoved.Values.ToList();
|
||||
|
||||
if (_libraryUpdateTimer is not null)
|
||||
{
|
||||
@@ -241,6 +245,15 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
|
||||
_foldersRemovedFrom.Clear();
|
||||
}
|
||||
|
||||
if (itemsAdded.Count == 0
|
||||
&& itemsUpdated.Count == 0
|
||||
&& itemsRemoved.Count == 0
|
||||
&& foldersAddedTo.Count == 0
|
||||
&& foldersRemovedFrom.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,15 +18,17 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
public sealed class UserDataChangeNotifier : IHostedService, IDisposable
|
||||
{
|
||||
private const int UpdateDuration = 500;
|
||||
internal const int MaxBatchSize = 2000;
|
||||
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
|
||||
private readonly Dictionary<Guid, Dictionary<Guid, BaseItem>> _changedItems = [];
|
||||
private readonly Lock _syncLock = new();
|
||||
|
||||
private Timer? _updateTimer;
|
||||
private int _changedItemCount;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserDataChangeNotifier"/> class.
|
||||
@@ -69,50 +71,64 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_updateTimer is null)
|
||||
{
|
||||
_updateTimer = new Timer(
|
||||
UpdateTimerCallback,
|
||||
null,
|
||||
UpdateDuration,
|
||||
Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
_updateTimer.Change(UpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
// The window runs from the first change of a batch and is never extended, so a stream
|
||||
// of changes that never pauses - a library scan - still closes its batches instead of
|
||||
// holding every item it touched alive until the stream stops.
|
||||
_updateTimer ??= new Timer(
|
||||
UpdateTimerCallback,
|
||||
null,
|
||||
UpdateDuration,
|
||||
Timeout.Infinite);
|
||||
|
||||
if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem>? keys))
|
||||
if (!_changedItems.TryGetValue(e.UserId, out Dictionary<Guid, BaseItem>? keys))
|
||||
{
|
||||
keys = new List<BaseItem>();
|
||||
keys = [];
|
||||
_changedItems[e.UserId] = keys;
|
||||
}
|
||||
|
||||
keys.Add(e.Item);
|
||||
|
||||
var baseItem = e.Item;
|
||||
|
||||
// Go up one level for indicators
|
||||
if (baseItem is not null)
|
||||
{
|
||||
Track(keys, baseItem);
|
||||
|
||||
var parent = baseItem.GetOwner() ?? baseItem.GetParent();
|
||||
|
||||
if (parent is not null)
|
||||
{
|
||||
keys.Add(parent);
|
||||
Track(keys, parent);
|
||||
}
|
||||
}
|
||||
|
||||
// A window long enough to cover a burst still has to give way once the batch is
|
||||
// large enough to be worth sending on its own.
|
||||
if (_changedItemCount >= MaxBatchSize)
|
||||
{
|
||||
_updateTimer.Change(0, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Track(Dictionary<Guid, BaseItem> keys, BaseItem item)
|
||||
{
|
||||
var before = keys.Count;
|
||||
keys[item.Id] = item;
|
||||
|
||||
if (keys.Count != before)
|
||||
{
|
||||
_changedItemCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private async void UpdateTimerCallback(object? state)
|
||||
{
|
||||
List<KeyValuePair<Guid, List<BaseItem>>> changes;
|
||||
List<KeyValuePair<Guid, Dictionary<Guid, BaseItem>>> changes;
|
||||
lock (_syncLock)
|
||||
{
|
||||
// Remove dupes in case some were saved multiple times
|
||||
changes = _changedItems.ToList();
|
||||
_changedItems.Clear();
|
||||
_changedItemCount = 0;
|
||||
|
||||
if (_updateTimer is not null)
|
||||
{
|
||||
@@ -121,17 +137,22 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (userId, changedItems) in changes)
|
||||
{
|
||||
await _sessionManager.SendMessageToUserSessions(
|
||||
[userId],
|
||||
SessionMessageType.UserDataChanged,
|
||||
() => GetUserDataChangeInfo(userId, changedItems),
|
||||
() => GetUserDataChangeInfo(userId, changedItems.Values),
|
||||
default).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)
|
||||
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, IEnumerable<BaseItem> changedItems)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId)
|
||||
?? throw new ArgumentException("Invalid user ID", nameof(userId));
|
||||
@@ -140,7 +161,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
UserId = userId,
|
||||
UserDataList = changedItems
|
||||
.DistinctBy(x => x.Id)
|
||||
.Select(i =>
|
||||
{
|
||||
var dto = _userDataManager.GetUserDataDto(i, user);
|
||||
|
||||
@@ -8,6 +8,7 @@ using Emby.Server.Implementations.Library;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -21,6 +22,7 @@ namespace Emby.Server.Implementations.IO
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
|
||||
|
||||
/// <summary>
|
||||
@@ -47,6 +49,7 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="configurationManager">The configuration manager.</param>
|
||||
/// <param name="fileSystem">The filesystem.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
/// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
|
||||
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
|
||||
public LibraryMonitor(
|
||||
@@ -54,6 +57,7 @@ namespace Emby.Server.Implementations.IO
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager configurationManager,
|
||||
IFileSystem fileSystem,
|
||||
IDirectoryService directoryService,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
|
||||
{
|
||||
@@ -61,6 +65,7 @@ namespace Emby.Server.Implementations.IO
|
||||
_logger = logger;
|
||||
_configurationManager = configurationManager;
|
||||
_fileSystem = fileSystem;
|
||||
_directoryService = directoryService;
|
||||
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
|
||||
|
||||
appLifetime.ApplicationStarted.Register(Start);
|
||||
@@ -363,6 +368,8 @@ namespace Emby.Server.Implementations.IO
|
||||
return;
|
||||
}
|
||||
|
||||
_directoryService.Invalidate(path);
|
||||
|
||||
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
|
||||
foreach (var i in _tempIgnoredPaths.Keys)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -15,7 +16,6 @@ using Emby.Naming.Common;
|
||||
using Emby.Naming.TV;
|
||||
using Emby.Naming.Video;
|
||||
using Emby.Server.Implementations.Library.Resolvers;
|
||||
using Emby.Server.Implementations.Library.Validators;
|
||||
using Emby.Server.Implementations.Playlists;
|
||||
using Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
using Emby.Server.Implementations.Sorting;
|
||||
@@ -35,7 +35,6 @@ using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@@ -45,6 +44,7 @@ using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Library;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@@ -74,7 +74,6 @@ namespace Emby.Server.Implementations.Library
|
||||
private readonly Lazy<IProviderManager> _providerManagerFactory;
|
||||
private readonly Lazy<IUserViewManager> _userViewManagerFactory;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IItemRepository _itemRepository;
|
||||
private readonly IItemPersistenceService _persistenceService;
|
||||
@@ -86,6 +85,8 @@ namespace Emby.Server.Implementations.Library
|
||||
private readonly IPeopleRepository _peopleRepository;
|
||||
private readonly ExtraResolver _extraResolver;
|
||||
private readonly IPathManager _pathManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly FastConcurrentLru<Guid, BaseItem> _cache;
|
||||
private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
|
||||
private readonly IMediaStreamRepository _mediaStreamRepository;
|
||||
@@ -120,7 +121,6 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
/// <param name="providerManagerFactory">The provider manager.</param>
|
||||
/// <param name="userViewManagerFactory">The user view manager.</param>
|
||||
/// <param name="mediaEncoder">The media encoder.</param>
|
||||
/// <param name="itemRepository">The item repository.</param>
|
||||
/// <param name="persistenceService">The item persistence service.</param>
|
||||
/// <param name="nextUpService">The next up service.</param>
|
||||
@@ -132,6 +132,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <param name="peopleRepository">The people repository.</param>
|
||||
/// <param name="pathManager">The path manager.</param>
|
||||
/// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
|
||||
/// <param name="localization">The localization manager.</param>
|
||||
/// <param name="mediaStreamRepository">The media stream repository.</param>
|
||||
/// <param name="externalDataManagerFactory">The external data manager (lazy, to break the DI cycle through ChapterManager).</param>
|
||||
public LibraryManager(
|
||||
@@ -145,7 +146,6 @@ namespace Emby.Server.Implementations.Library
|
||||
IFileSystem fileSystem,
|
||||
Lazy<IProviderManager> providerManagerFactory,
|
||||
Lazy<IUserViewManager> userViewManagerFactory,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IItemRepository itemRepository,
|
||||
IItemPersistenceService persistenceService,
|
||||
INextUpService nextUpService,
|
||||
@@ -157,6 +157,7 @@ namespace Emby.Server.Implementations.Library
|
||||
IPeopleRepository peopleRepository,
|
||||
IPathManager pathManager,
|
||||
DotIgnoreIgnoreRule dotIgnoreIgnoreRule,
|
||||
ILocalizationManager localization,
|
||||
IMediaStreamRepository mediaStreamRepository,
|
||||
Lazy<IExternalDataManager> externalDataManagerFactory)
|
||||
{
|
||||
@@ -170,7 +171,6 @@ namespace Emby.Server.Implementations.Library
|
||||
_fileSystem = fileSystem;
|
||||
_providerManagerFactory = providerManagerFactory;
|
||||
_userViewManagerFactory = userViewManagerFactory;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_itemRepository = itemRepository;
|
||||
_persistenceService = persistenceService;
|
||||
_nextUpService = nextUpService;
|
||||
@@ -184,6 +184,8 @@ namespace Emby.Server.Implementations.Library
|
||||
_peopleRepository = peopleRepository;
|
||||
_pathManager = pathManager;
|
||||
_dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
|
||||
_localization = localization;
|
||||
_directoryService = directoryService;
|
||||
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
|
||||
|
||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||
@@ -407,6 +409,13 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
_persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
|
||||
|
||||
// Evict the deleted items from the cache and announce each removal.
|
||||
foreach (var (item, _, _) in pathMaps)
|
||||
{
|
||||
_cache.TryRemove(item.Id, out _);
|
||||
ReportItemRemoved(item, item.GetOwner() ?? item.GetParent());
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
|
||||
@@ -606,6 +615,12 @@ namespace Emby.Server.Implementations.Library
|
||||
folder.UserData = null;
|
||||
}
|
||||
|
||||
// Announce the descendants before the item itself.
|
||||
foreach (var child in children)
|
||||
{
|
||||
ReportItemRemoved(child, item);
|
||||
}
|
||||
|
||||
ReportItemRemoved(item, parent);
|
||||
}
|
||||
|
||||
@@ -1191,6 +1206,12 @@ namespace Emby.Server.Implementations.Library
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetPersonId(string name)
|
||||
{
|
||||
return GetItemByNameId<Person>(Person.GetPath(name));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Person? GetPerson(string name)
|
||||
{
|
||||
@@ -1204,6 +1225,33 @@ namespace Emby.Server.Implementations.Library
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Person GetOrCreatePerson(string name)
|
||||
{
|
||||
var existing = GetPerson(name);
|
||||
if (existing is not null)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var path = Person.GetPath(name);
|
||||
var info = Directory.CreateDirectory(path);
|
||||
var item = new Person
|
||||
{
|
||||
Name = name,
|
||||
Id = GetItemByNameId<Person>(path),
|
||||
DateCreated = info.CreationTimeUtc,
|
||||
DateModified = info.LastWriteTimeUtc,
|
||||
Path = path
|
||||
};
|
||||
|
||||
item.PresentationUniqueKey = item.CreatePresentationUniqueKey();
|
||||
|
||||
CreateItem(item, null);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the studio.
|
||||
/// </summary>
|
||||
@@ -1336,15 +1384,6 @@ namespace Emby.Server.Implementations.Library
|
||||
return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
// Ensure the location is available.
|
||||
Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
|
||||
|
||||
return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the root media folder.
|
||||
/// </summary>
|
||||
@@ -1471,6 +1510,10 @@ namespace Emby.Server.Implementations.Library
|
||||
var numComplete = 0;
|
||||
var numTasks = tasks.Count;
|
||||
|
||||
_logger.LogInformation("Running {TaskCount} post-scan task(s)", numTasks);
|
||||
|
||||
var phaseStart = Stopwatch.GetTimestamp();
|
||||
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
// Prevent access to modified closure
|
||||
@@ -1488,20 +1531,45 @@ namespace Emby.Server.Implementations.Library
|
||||
progress.Report(innerPercent);
|
||||
});
|
||||
|
||||
_logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
|
||||
var taskName = task.GetType().Name;
|
||||
var taskStart = Stopwatch.GetTimestamp();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Running post-scan task {TaskNumber}/{TaskCount}: {TaskName}",
|
||||
currentNumComplete + 1,
|
||||
numTasks,
|
||||
taskName);
|
||||
|
||||
try
|
||||
{
|
||||
await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var elapsed = Stopwatch.GetElapsedTime(taskStart);
|
||||
_logger.LogInformation(
|
||||
"Post-scan task {TaskName} completed after {Minutes} minute(s) and {Seconds} seconds",
|
||||
taskName,
|
||||
Math.Truncate(elapsed.TotalMinutes),
|
||||
elapsed.Seconds);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
|
||||
var elapsed = Stopwatch.GetElapsedTime(taskStart);
|
||||
_logger.LogInformation(
|
||||
"Post-scan task {TaskName} cancelled after {Minutes} minute(s) and {Seconds} seconds",
|
||||
taskName,
|
||||
Math.Truncate(elapsed.TotalMinutes),
|
||||
elapsed.Seconds);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error running post-scan task");
|
||||
var elapsed = Stopwatch.GetElapsedTime(taskStart);
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Post-scan task {TaskName} failed after {Minutes} minute(s) and {Seconds} seconds",
|
||||
taskName,
|
||||
Math.Truncate(elapsed.TotalMinutes),
|
||||
elapsed.Seconds);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
@@ -1510,6 +1578,12 @@ namespace Emby.Server.Implementations.Library
|
||||
progress.Report(percent * 100);
|
||||
}
|
||||
|
||||
var phaseElapsed = Stopwatch.GetElapsedTime(phaseStart);
|
||||
_logger.LogInformation(
|
||||
"All post-scan tasks completed after {Minutes} minute(s) and {Seconds} seconds",
|
||||
Math.Truncate(phaseElapsed.TotalMinutes),
|
||||
phaseElapsed.Seconds);
|
||||
|
||||
_persistenceService.UpdateInheritedValues();
|
||||
|
||||
progress.Report(100);
|
||||
@@ -1727,9 +1801,21 @@ namespace Emby.Server.Implementations.Library
|
||||
return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
|
||||
}
|
||||
|
||||
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
|
||||
/// <inheritdoc/>
|
||||
public Dictionary<Guid, ItemCounts> GetItemCountsForNameItems(BaseItemKind kind, IReadOnlyList<Guid> ids, BaseItemKind[] relatedItemKinds, User? user)
|
||||
{
|
||||
return _countService.GetChildCountBatch(parentIds, userId);
|
||||
var query = new InternalItemsQuery(user);
|
||||
if (user is not null)
|
||||
{
|
||||
AddUserToQuery(query, user);
|
||||
}
|
||||
|
||||
return _countService.GetItemCountsForNameItems(kind, ids, relatedItemKinds, query);
|
||||
}
|
||||
|
||||
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, User? user)
|
||||
{
|
||||
return _countService.GetChildCountBatch(parentIds, user);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -1896,14 +1982,14 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
// Optimize by querying against top level views
|
||||
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
|
||||
query.AncestorIds = [];
|
||||
|
||||
// Prevent searching in all libraries due to empty filter
|
||||
if (query.TopParentIds.Length == 0)
|
||||
var topParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
|
||||
if (topParentIds.Length == 0)
|
||||
{
|
||||
query.TopParentIds = [Guid.NewGuid()];
|
||||
return;
|
||||
}
|
||||
|
||||
query.TopParentIds = topParentIds;
|
||||
query.AncestorIds = [];
|
||||
}
|
||||
|
||||
public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
|
||||
@@ -1949,12 +2035,15 @@ namespace Emby.Server.Implementations.Library
|
||||
if (parents.All(i => i is ICollectionFolder || i is UserView))
|
||||
{
|
||||
// Optimize by querying against top level views
|
||||
query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
|
||||
var topParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
|
||||
|
||||
// Prevent searching in all libraries due to empty filter
|
||||
if (query.TopParentIds.Length == 0)
|
||||
if (topParentIds.Length > 0)
|
||||
{
|
||||
query.TopParentIds = [Guid.NewGuid()];
|
||||
query.TopParentIds = topParentIds;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetAncestorIds(query, parents);
|
||||
}
|
||||
}
|
||||
else if (parents.Count == 1 && parents.First() is Folder folder
|
||||
@@ -1963,34 +2052,31 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
// Playlists and BoxSets store their contents in LinkedChildren and never
|
||||
// populate AncestorIds for those items, so a recursive AncestorIds query
|
||||
// would return zero rows. Resolve to the linked child IDs up front and
|
||||
// route through the existing indexed ItemIds filter.
|
||||
query.ItemIds = folder.LinkedChildren
|
||||
.Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty())
|
||||
.Select(lc => lc.ItemId!.Value)
|
||||
.ToArray();
|
||||
|
||||
// Empty linked-children should still return empty rather than scanning everything.
|
||||
if (query.ItemIds.Length == 0)
|
||||
{
|
||||
query.ItemIds = [Guid.NewGuid()];
|
||||
}
|
||||
// would return zero rows. Filter by the descendant set instead, which follows
|
||||
// the links and keeps descending, so a linked folder contributes what is below
|
||||
// it as well - the episodes of a Series added to a collection, for example.
|
||||
query.DescendantOfId = folder.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We need to be able to query from any arbitrary ancestor up the tree
|
||||
query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
|
||||
|
||||
// Prevent searching in all libraries due to empty filter
|
||||
if (query.AncestorIds.Length == 0)
|
||||
{
|
||||
query.AncestorIds = [Guid.NewGuid()];
|
||||
}
|
||||
SetAncestorIds(query, parents);
|
||||
}
|
||||
|
||||
query.Parent = null;
|
||||
}
|
||||
|
||||
private static void SetAncestorIds(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
|
||||
{
|
||||
// We need to be able to query from any arbitrary ancestor up the tree
|
||||
query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
|
||||
|
||||
// Prevent searching in all libraries due to empty filter
|
||||
if (query.AncestorIds.Length == 0)
|
||||
{
|
||||
query.AncestorIds = [Guid.NewGuid()];
|
||||
}
|
||||
}
|
||||
|
||||
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
|
||||
{
|
||||
if (query.User is null)
|
||||
@@ -2229,6 +2315,12 @@ namespace Emby.Server.Implementations.Library
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlySet<Guid> GetItemIdsWithAlternateVersions(IReadOnlyList<Guid> itemIds)
|
||||
{
|
||||
return _linkedChildrenService.GetItemIdsWithAlternateVersions(itemIds);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType childType)
|
||||
{
|
||||
@@ -2315,9 +2407,13 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var comparer = Comparers.FirstOrDefault(c => name == c.Type);
|
||||
|
||||
// If it requires a user, create a new one, and assign the user
|
||||
if (comparer is IUserBaseItemComparer)
|
||||
{
|
||||
if (user is null)
|
||||
{
|
||||
throw new ArgumentException($"Sort key '{name}' requires a user, but none was provided.");
|
||||
}
|
||||
|
||||
var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null for Nullable<T> instances
|
||||
|
||||
userComparer.User = user;
|
||||
@@ -2367,6 +2463,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
altVideo.OwnerId = video.Id;
|
||||
altVideo.SetPrimaryVersionId(video.Id);
|
||||
altVideo.IsInMixedFolder = video.IsInMixedFolder;
|
||||
// ResolveAlternateVersion only sees the alternate's primary file.
|
||||
// If the alternate is itself a stack (e.g. 1080p part1 + part2),
|
||||
// detect its parts from sibling files so its AdditionalParts persist.
|
||||
@@ -2490,9 +2587,15 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
if (!File.Exists(image.Path))
|
||||
if (string.IsNullOrEmpty(image.Path) || !File.Exists(image.Path))
|
||||
{
|
||||
_logger.LogWarning("Image not found at {ImagePath}", image.Path);
|
||||
_logger.LogWarning(
|
||||
"{ImageType} image for {ItemName} ({ItemId}) not found at \"{ImagePath}\", source was {SourcePath}",
|
||||
img.Type,
|
||||
item.Name,
|
||||
item.Id,
|
||||
image.Path,
|
||||
img.Path);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2552,6 +2655,8 @@ namespace Emby.Server.Implementations.Library
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
ForgetDroppedLocalAlternateVersions(items);
|
||||
|
||||
// Resolve and add any local alternate version items that don't exist yet
|
||||
// This ensures they exist in the database when LinkedChildren are processed
|
||||
var allItems = new List<BaseItem>(items);
|
||||
@@ -2580,6 +2685,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
altVideo.OwnerId = video.Id;
|
||||
altVideo.SetPrimaryVersionId(video.Id);
|
||||
altVideo.IsInMixedFolder = video.IsInMixedFolder;
|
||||
// ResolveAlternateVersion only sees the alternate's primary file.
|
||||
// If the alternate is itself a stack (e.g. 1080p part1 + part2),
|
||||
// detect its parts from sibling files so its AdditionalParts persist.
|
||||
@@ -2640,6 +2746,30 @@ namespace Emby.Server.Implementations.Library
|
||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Forgets the cached local alternate versions of the supplied items that they no longer list.
|
||||
/// </summary>
|
||||
/// <param name="items">The items about to be saved.</param>
|
||||
private void ForgetDroppedLocalAlternateVersions(IReadOnlyList<BaseItem> items)
|
||||
{
|
||||
foreach (var video in items.OfType<Video>())
|
||||
{
|
||||
var videoType = video.GetType();
|
||||
var keptIds = video.LocalAlternateVersions
|
||||
.Where(path => !string.IsNullOrEmpty(path))
|
||||
.Select(path => GetNewItemId(path, videoType))
|
||||
.ToHashSet();
|
||||
|
||||
foreach (var versionId in GetLocalAlternateVersionIds(video))
|
||||
{
|
||||
if (!keptIds.Contains(versionId))
|
||||
{
|
||||
_cache.TryRemove(versionId, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -2863,7 +2993,8 @@ namespace Emby.Server.Implementations.Library
|
||||
"views",
|
||||
_fileSystem.GetValidFilename(viewType.ToString()));
|
||||
|
||||
var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
|
||||
// The display name is localized, so it must not take part in the id.
|
||||
var id = GetNewItemId(path + "_namedview_" + viewType.ToString(), typeof(UserView));
|
||||
|
||||
var item = GetItemById(id) as UserView;
|
||||
|
||||
@@ -2887,6 +3018,13 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
refresh = true;
|
||||
}
|
||||
else if (!string.Equals(item.Name, name, StringComparison.Ordinal))
|
||||
{
|
||||
item.Name = name;
|
||||
item.ForcedSortName = sortName;
|
||||
|
||||
refresh = true;
|
||||
}
|
||||
|
||||
if (refresh)
|
||||
{
|
||||
@@ -2907,7 +3045,9 @@ namespace Emby.Server.Implementations.Library
|
||||
var parentIdString = parentId.IsEmpty()
|
||||
? null
|
||||
: parentId.ToString("N", CultureInfo.InvariantCulture);
|
||||
var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
|
||||
|
||||
// The name is either localized (grouped views) or the library folder's own name.
|
||||
var idValues = "38_namedview_" + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
|
||||
|
||||
var id = GetNewItemId(idValues, typeof(UserView));
|
||||
|
||||
@@ -2937,6 +3077,11 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
isNew = true;
|
||||
}
|
||||
else if (!string.Equals(item.Name, name, StringComparison.Ordinal))
|
||||
{
|
||||
item.Name = name;
|
||||
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
var lastRefreshedUtc = item.DateLastRefreshed;
|
||||
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
|
||||
@@ -3038,7 +3183,7 @@ namespace Emby.Server.Implementations.Library
|
||||
var parentIdString = parentId.IsEmpty()
|
||||
? null
|
||||
: parentId.ToString("N", CultureInfo.InvariantCulture);
|
||||
var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
|
||||
var idValues = "37_namedview_" + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
|
||||
if (!string.IsNullOrEmpty(uniqueId))
|
||||
{
|
||||
idValues += uniqueId;
|
||||
@@ -3072,9 +3217,10 @@ namespace Emby.Server.Implementations.Library
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
if (viewType != item.ViewType)
|
||||
if (viewType != item.ViewType || !string.Equals(item.Name, name, StringComparison.Ordinal))
|
||||
{
|
||||
item.ViewType = viewType;
|
||||
item.Name = name;
|
||||
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
@@ -3195,11 +3341,11 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
if (!episode.ProductionYear.HasValue)
|
||||
if (episode.ProductionYear is null)
|
||||
{
|
||||
episode.ProductionYear = episodeInfo.Year;
|
||||
|
||||
if (episode.ProductionYear.HasValue)
|
||||
if (episode.ProductionYear is not null)
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
@@ -3276,9 +3422,11 @@ namespace Emby.Server.Implementations.Library
|
||||
var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath);
|
||||
if (ownerVideoInfo is null)
|
||||
{
|
||||
yield break;
|
||||
return [];
|
||||
}
|
||||
|
||||
var candidates = new List<ExtraCandidate>();
|
||||
|
||||
var count = filtered.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
@@ -3292,35 +3440,50 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
foreach (var file in filesInSubFolderList)
|
||||
{
|
||||
if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType))
|
||||
if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType, out var extraRule))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var extra = GetExtra(file, extraType.Value, subFolderIsMixedFolder);
|
||||
if (extra is not null)
|
||||
{
|
||||
yield return extra;
|
||||
}
|
||||
AddCandidate(file, extraType.Value, extraRule, subFolderIsMixedFolder);
|
||||
}
|
||||
}
|
||||
else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType))
|
||||
else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType, out var extraRule))
|
||||
{
|
||||
var extra = GetExtra(current, extraType.Value, false);
|
||||
if (extra is not null)
|
||||
{
|
||||
yield return extra;
|
||||
}
|
||||
AddCandidate(current, extraType.Value, extraRule, false);
|
||||
}
|
||||
}
|
||||
|
||||
BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType, bool isInMixedFolder)
|
||||
var extras = new List<BaseItem>();
|
||||
var typeCounters = new Dictionary<ExtraType, int>();
|
||||
|
||||
// Order by path so that the numbering handed out below does not depend on the
|
||||
// order the file system happened to list the folder in
|
||||
foreach (var candidate in candidates.OrderBy(c => c.Extra.Path, StringComparer.Ordinal))
|
||||
{
|
||||
var extra = PrepareExtra(candidate);
|
||||
if (extra is not null)
|
||||
{
|
||||
extras.Add(extra);
|
||||
}
|
||||
}
|
||||
|
||||
return extras;
|
||||
|
||||
void AddCandidate(FileSystemMetadata file, ExtraType extraType, ExtraRule extraRule, bool isInMixedFolder)
|
||||
{
|
||||
var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetResolversForExtraType(extraType));
|
||||
if (extra is not Video && extra is not Audio)
|
||||
if (extra is Video or Audio)
|
||||
{
|
||||
return null;
|
||||
candidates.Add(new ExtraCandidate(extra, extraType, extraRule, isInMixedFolder));
|
||||
}
|
||||
}
|
||||
|
||||
BaseItem? PrepareExtra(ExtraCandidate candidate)
|
||||
{
|
||||
var resolved = candidate.Extra;
|
||||
var extra = resolved;
|
||||
var name = GetExtraName(candidate, ownerVideoInfo, typeCounters);
|
||||
|
||||
// Try to retrieve it from the db. If we don't find it, use the resolved version
|
||||
var itemById = GetItemById(extra.Id);
|
||||
@@ -3329,10 +3492,18 @@ namespace Emby.Server.Implementations.Library
|
||||
extra = itemById;
|
||||
}
|
||||
|
||||
// Only update extra type if it is more specific then the currently known extra type
|
||||
if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
|
||||
// An extra is named after its file, so the file is the source of truth. Items created
|
||||
// by older versions, or renamed by a metadata provider, are corrected here;
|
||||
// RefreshExtras persists the change.
|
||||
if (!string.IsNullOrEmpty(name) && extra.LockedFields?.Contains(MetadataField.Name) != true)
|
||||
{
|
||||
extra.ExtraType = extraType;
|
||||
extra.Name = name;
|
||||
}
|
||||
|
||||
// Only update extra type if it is more specific then the currently known extra type
|
||||
if (extra.ExtraType is null or ExtraType.Unknown || candidate.ExtraType != ExtraType.Unknown)
|
||||
{
|
||||
extra.ExtraType = candidate.ExtraType;
|
||||
}
|
||||
|
||||
// Only return items that are actual extras (have ExtraType set)
|
||||
@@ -3340,7 +3511,7 @@ namespace Emby.Server.Implementations.Library
|
||||
// so that RefreshExtras can detect when they need updating and set ForceSave.
|
||||
if (extra.ExtraType is not null)
|
||||
{
|
||||
extra.IsInMixedFolder = isInMixedFolder;
|
||||
extra.IsInMixedFolder = candidate.IsInMixedFolder;
|
||||
return extra;
|
||||
}
|
||||
|
||||
@@ -3348,6 +3519,57 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name to give an extra.
|
||||
/// </summary>
|
||||
/// <param name="candidate">The resolved extra.</param>
|
||||
/// <param name="ownerVideoInfo">The naming info of the owner.</param>
|
||||
/// <param name="typeCounters">Number of extras named after their type so far, per type.</param>
|
||||
/// <returns>The name.</returns>
|
||||
private string GetExtraName(ExtraCandidate candidate, VideoFileInfo ownerVideoInfo, Dictionary<ExtraType, int> typeCounters)
|
||||
{
|
||||
var isNamedAfterOwner = candidate.ExtraRule.RuleType switch
|
||||
{
|
||||
ExtraRuleType.Filename => true,
|
||||
ExtraRuleType.Suffix => string.Equals(candidate.Extra.Name, ownerVideoInfo.Name, StringComparison.OrdinalIgnoreCase),
|
||||
_ => false
|
||||
};
|
||||
|
||||
if (!isNamedAfterOwner)
|
||||
{
|
||||
return candidate.Extra.Name;
|
||||
}
|
||||
|
||||
typeCounters.TryGetValue(candidate.ExtraType, out var seen);
|
||||
typeCounters[candidate.ExtraType] = seen + 1;
|
||||
|
||||
var typeName = _localization.GetServerLocalizedString(GetExtraTypeNameKey(candidate.ExtraType));
|
||||
|
||||
return seen == 0
|
||||
? typeName
|
||||
: string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetServerLocalizedString("NameExtraNumbered"),
|
||||
typeName,
|
||||
seen + 1);
|
||||
}
|
||||
|
||||
private static string GetExtraTypeNameKey(ExtraType extraType) => extraType switch
|
||||
{
|
||||
ExtraType.Clip => "NameExtraClip",
|
||||
ExtraType.Trailer => "NameExtraTrailer",
|
||||
ExtraType.BehindTheScenes => "NameExtraBehindTheScenes",
|
||||
ExtraType.DeletedScene => "NameExtraDeletedScene",
|
||||
ExtraType.Interview => "NameExtraInterview",
|
||||
ExtraType.Scene => "NameExtraScene",
|
||||
ExtraType.Sample => "NameExtraSample",
|
||||
ExtraType.ThemeSong => "NameExtraThemeSong",
|
||||
ExtraType.ThemeVideo => "NameExtraThemeVideo",
|
||||
ExtraType.Featurette => "NameExtraFeaturette",
|
||||
ExtraType.Short => "NameExtraShort",
|
||||
_ => "NameExtraUnknown"
|
||||
};
|
||||
|
||||
public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
|
||||
{
|
||||
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
|
||||
@@ -3418,12 +3640,24 @@ namespace Emby.Server.Implementations.Library
|
||||
return _peopleRepository.GetPeopleNames(query);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int DeleteOrphanedCredits()
|
||||
{
|
||||
return _peopleRepository.DeleteOrphanedCredits();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IReadOnlyList<string> personTypes)
|
||||
{
|
||||
return _peopleRepository.GetPeopleNamesByItems(itemIds, personTypes);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>> GetPeopleByItems(IReadOnlyList<Guid> itemIds)
|
||||
{
|
||||
return _peopleRepository.GetPeopleByItems(itemIds);
|
||||
}
|
||||
|
||||
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
|
||||
{
|
||||
UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
|
||||
@@ -3457,7 +3691,20 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return item.GetImageInfo(image.Type, imageIndex);
|
||||
var localImage = item.GetImageInfo(image.Type, imageIndex);
|
||||
if (localImage is null)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Downloaded {0} image {1} from {2} is not attached to {3} ({4})",
|
||||
image.Type,
|
||||
imageIndex,
|
||||
url,
|
||||
item.Name,
|
||||
item.Id));
|
||||
}
|
||||
|
||||
return localImage;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
@@ -3479,7 +3726,13 @@ namespace Emby.Server.Implementations.Library
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unable to convert any images to local");
|
||||
throw new InvalidOperationException(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Unable to convert any {0} image url in \"{1}\" to a local file for {2} ({3})",
|
||||
image.Type,
|
||||
image.Path,
|
||||
item.Name,
|
||||
item.Id));
|
||||
}
|
||||
|
||||
public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
|
||||
@@ -3535,6 +3788,10 @@ namespace Emby.Server.Implementations.Library
|
||||
AddMediaPathInternal(name, path, false);
|
||||
}
|
||||
}
|
||||
|
||||
// The libraries root was listed before this folder existed, so drop that listing:
|
||||
// anything still reading it resolves the library set without the new folder.
|
||||
_directoryService.Invalidate(virtualFolderPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -3561,27 +3818,14 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var itemUpdateType = ItemUpdateType.MetadataDownload;
|
||||
var saveEntity = false;
|
||||
var createEntity = false;
|
||||
var personEntity = GetPerson(person.Name);
|
||||
|
||||
if (personEntity is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Person.GetPath(person.Name);
|
||||
var info = Directory.CreateDirectory(path);
|
||||
personEntity = new Person()
|
||||
{
|
||||
Name = person.Name,
|
||||
Id = GetItemByNameId<Person>(path),
|
||||
DateCreated = info.CreationTimeUtc,
|
||||
DateModified = info.LastWriteTimeUtc,
|
||||
Path = path
|
||||
};
|
||||
|
||||
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
||||
personEntity = GetOrCreatePerson(person.Name);
|
||||
saveEntity = true;
|
||||
createEntity = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -3615,11 +3859,6 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (saveEntity)
|
||||
{
|
||||
if (createEntity)
|
||||
{
|
||||
CreateItems([personEntity], null, CancellationToken.None);
|
||||
}
|
||||
|
||||
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
|
||||
personEntity.DateLastSaved = DateTime.UtcNow;
|
||||
|
||||
@@ -3659,7 +3898,9 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName)
|
||||
?? throw new FileNotFoundException(
|
||||
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
|
||||
|
||||
CreateShortcut(virtualFolderPath, pathInfo);
|
||||
|
||||
@@ -3680,7 +3921,9 @@ namespace Emby.Server.Implementations.Library
|
||||
ArgumentNullException.ThrowIfNull(mediaPath);
|
||||
|
||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName)
|
||||
?? throw new FileNotFoundException(
|
||||
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
|
||||
|
||||
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
|
||||
|
||||
@@ -3719,9 +3962,9 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
|
||||
var path = Path.Combine(rootFolderPath, name);
|
||||
var path = FileSystemHelper.GetChildPath(rootFolderPath, name);
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
if (path is null || !Directory.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException("The media folder does not exist");
|
||||
}
|
||||
@@ -3731,6 +3974,7 @@ namespace Emby.Server.Implementations.Library
|
||||
try
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
_directoryService.Invalidate(path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -3785,9 +4029,9 @@ namespace Emby.Server.Implementations.Library
|
||||
ArgumentException.ThrowIfNullOrEmpty(mediaPath);
|
||||
|
||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
var virtualFolderPath = FileSystemHelper.GetChildPath(rootFolderPath, virtualFolderName);
|
||||
|
||||
if (!Directory.Exists(virtualFolderPath))
|
||||
if (virtualFolderPath is null || !Directory.Exists(virtualFolderPath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolderName));
|
||||
@@ -3800,6 +4044,7 @@ namespace Emby.Server.Implementations.Library
|
||||
if (!string.IsNullOrEmpty(shortcut))
|
||||
{
|
||||
_fileSystem.DeleteFile(shortcut);
|
||||
_directoryService.Invalidate(shortcut);
|
||||
}
|
||||
|
||||
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
|
||||
@@ -3843,6 +4088,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
|
||||
_directoryService.Invalidate(lnk);
|
||||
RemoveContentTypeOverrides(path);
|
||||
}
|
||||
|
||||
@@ -3898,5 +4144,7 @@ namespace Emby.Server.Implementations.Library
|
||||
SetTopParentOrAncestorIds(query);
|
||||
return _itemRepository.GetMediaStreamLanguages(query, mediaStreamType);
|
||||
}
|
||||
|
||||
private sealed record ExtraCandidate(BaseItem Extra, ExtraType ExtraType, ExtraRule ExtraRule, bool IsInMixedFolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
_namingOptions = namingOptions;
|
||||
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
|
||||
_videoResolvers = new IItemResolver[] { this };
|
||||
_trailerResolvers = [new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService, parseName: true)];
|
||||
_videoResolvers = [this];
|
||||
}
|
||||
|
||||
protected override Video Resolve(ItemResolveArgs args)
|
||||
@@ -54,12 +54,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
_ => _videoResolvers
|
||||
};
|
||||
|
||||
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
|
||||
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, [NotNullWhen(true)] out ExtraRule? extraRule, string? libraryRoot = "")
|
||||
{
|
||||
var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
|
||||
if (extraResult.ExtraType is null)
|
||||
if (extraResult.ExtraType is null || extraResult.Rule is null)
|
||||
{
|
||||
extraType = null;
|
||||
extraRule = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -88,6 +89,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
}
|
||||
|
||||
extraType = extraResult.ExtraType;
|
||||
extraRule = extraResult.Rule;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -14,15 +15,25 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
public class GenericVideoResolver<T> : BaseVideoResolver<T>
|
||||
where T : Video, new()
|
||||
{
|
||||
private readonly bool _parseName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenericVideoResolver{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
/// <param name="parseName">Whether to parse the file name for metadata such as the year.</param>
|
||||
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService, bool parseName = false)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
_parseName = parseName;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override T Resolve(ItemResolveArgs args)
|
||||
{
|
||||
return ResolveVideo<T>(args, _parseName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Server.Implementations.Playlists;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -46,6 +47,19 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
};
|
||||
}
|
||||
|
||||
// Anything directly inside the internal playlists folder is a playlist, even when its
|
||||
// playlist.xml is missing: failing to resolve here makes the library scan treat the
|
||||
// playlist as deleted from disk and remove it, taking its items with it.
|
||||
if (args.Parent is PlaylistsFolder)
|
||||
{
|
||||
return new Playlist
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = filename,
|
||||
OpenAccess = true
|
||||
};
|
||||
}
|
||||
|
||||
// It's a directory-based playlist if the directory contains a playlist file
|
||||
IEnumerable<string> filePaths;
|
||||
try
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
args.LibraryOptions.SeasonZeroDisplayName :
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("NameSeasonNumber"),
|
||||
_localization.GetServerLocalizedString("NameSeasonNumber"),
|
||||
seasonNumber,
|
||||
args.LibraryOptions.PreferredMetadataLanguage);
|
||||
}
|
||||
@@ -129,6 +129,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
|
||||
var tmdbId = justName.GetAttributeValue("tmdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||
|
||||
// Anime databases model a single cour as its own entry, so a multi-season
|
||||
// series maps to one of these ids per season rather than one per series.
|
||||
var anidbId = justName.GetAttributeValue("anidbid");
|
||||
item.TrySetProviderId("AniDB", anidbId);
|
||||
|
||||
var aniListId = justName.GetAttributeValue("anilistid");
|
||||
item.TrySetProviderId("AniList", aniListId);
|
||||
|
||||
var aniSearchId = justName.GetAttributeValue("anisearchid");
|
||||
item.TrySetProviderId("AniSearch", aniSearchId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,49 +92,45 @@ public class SearchManager : ISearchManager
|
||||
await Task.WhenAll(externalTask, internalTask).ConfigureAwait(false);
|
||||
|
||||
var externalResults = await externalTask.ConfigureAwait(false);
|
||||
var fromExternal = externalResults.Count > 0;
|
||||
IReadOnlyList<SearchResult> results;
|
||||
if (fromExternal)
|
||||
{
|
||||
results = externalResults;
|
||||
}
|
||||
else
|
||||
{
|
||||
results = await internalTask.ConfigureAwait(false);
|
||||
if (_internalProviders.Length > 0)
|
||||
{
|
||||
_logger.LogDebug("No results from external providers, using internal provider results");
|
||||
}
|
||||
}
|
||||
|
||||
// Internal providers apply user-access filtering inline in their queries. External
|
||||
// providers don't know about user permissions, so they may return IDs from hidden
|
||||
// libraries or items the user is otherwise blocked from. Run the post-filter only
|
||||
// when results came from externals to close that gap. The Items controller's second
|
||||
// roundtrip via folder.GetItems applies most of these again, but it does not restrict
|
||||
// by TopParentIds when ItemIds is set.
|
||||
if (fromExternal && results.Count > 0 && query.UserId.HasValue && !query.UserId.Value.IsEmpty())
|
||||
// libraries or items the user is otherwise blocked from. Filter them here to close
|
||||
// that gap. The Items controller's second roundtrip via folder.GetItems applies most
|
||||
// of these again, but it does not restrict by TopParentIds when ItemIds is set.
|
||||
if (externalResults.Count > 0 && query.UserId.HasValue && !query.UserId.Value.IsEmpty())
|
||||
{
|
||||
var user = _userManager.GetUserById(query.UserId.Value);
|
||||
if (user is not null)
|
||||
{
|
||||
results = await FilterByUserAccessAsync(results, user, cancellationToken).ConfigureAwait(false);
|
||||
externalResults = await FilterByUserAccessAsync(externalResults, user, query, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
if (externalResults.Count > 0)
|
||||
{
|
||||
return externalResults;
|
||||
}
|
||||
|
||||
if (_internalProviders.Length > 0)
|
||||
{
|
||||
_logger.LogDebug("No results from external providers, using internal provider results");
|
||||
}
|
||||
|
||||
return await internalTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<SearchResult>> FilterByUserAccessAsync(
|
||||
IReadOnlyList<SearchResult> candidates,
|
||||
User user,
|
||||
SearchProviderQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// SetUser populates parental rating + blocked/allowed tags. ConfigureUserAccess populates
|
||||
// TopParentIds for the user's accessible libraries — we call it before assigning ItemIds
|
||||
// because LibraryManager.AddUserToQuery skips TopParentIds when ItemIds is non-empty.
|
||||
var accessFilter = new InternalItemsQuery(user);
|
||||
_libraryManager.ConfigureUserAccess(accessFilter, user);
|
||||
// SetUser populates parental rating + blocked/allowed tags, Build populates TopParentIds
|
||||
// for the user's accessible libraries. The candidate ids are applied to the query below
|
||||
// rather than to the filter because LibraryManager.AddUserToQuery skips TopParentIds when
|
||||
// ItemIds is non-empty.
|
||||
var accessFilter = SearchQueryAccessFilter.Build(user, query, _libraryManager);
|
||||
|
||||
Guid[] candidateIds = [.. candidates.Select(c => c.ItemId)];
|
||||
|
||||
@@ -147,17 +143,16 @@ public class SearchManager : ISearchManager
|
||||
|
||||
baseQuery = _queryHelpers.ApplyAccessFiltering(dbContext, baseQuery, accessFilter);
|
||||
|
||||
var allowedCount = await baseQuery.CountAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (allowedCount == candidates.Count)
|
||||
{
|
||||
return candidates;
|
||||
}
|
||||
|
||||
var allowedIds = await baseQuery
|
||||
.Select(e => e.Id)
|
||||
.ToHashSetAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (allowedIds.Count == candidates.Count)
|
||||
{
|
||||
return candidates;
|
||||
}
|
||||
|
||||
var filtered = candidates.Where(c => allowedIds.Contains(c.ItemId)).ToList();
|
||||
if (filtered.Count < candidates.Count)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Search;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the access filter that decides which items a search may return for a user.
|
||||
/// </summary>
|
||||
internal static class SearchQueryAccessFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds an access filter carrying the search's library access and type filters.
|
||||
/// </summary>
|
||||
/// <param name="user">The user the search runs for.</param>
|
||||
/// <param name="query">The search query.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <returns>The access filter.</returns>
|
||||
public static InternalItemsQuery Build(User user, SearchProviderQuery query, ILibraryManager libraryManager)
|
||||
{
|
||||
// The type filters have to travel with the access filter: a by-name item belongs to no
|
||||
// library, so it carries no TopParentId to match, and the library filter only knows to
|
||||
// exempt it when the query says those types are wanted. A search scoped to a parent gets
|
||||
// no exemption because a by-name item has no parent to descend from either.
|
||||
var accessFilter = new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = query.IncludeItemTypes,
|
||||
ExcludeItemTypes = query.ExcludeItemTypes,
|
||||
IncludeItemsByName = !query.ParentId.HasValue || query.ParentId.Value.IsEmpty()
|
||||
};
|
||||
|
||||
// ConfigureUserAccess populates TopParentIds for the libraries the user may open.
|
||||
libraryManager.ConfigureUserAccess(accessFilter, user);
|
||||
|
||||
return accessFilter;
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ public class SqlSearchProvider : IInternalSearchProvider
|
||||
dbQuery = ApplyTypeFilter(dbQuery, query.IncludeItemTypes, query.ExcludeItemTypes);
|
||||
dbQuery = ApplyMediaTypeFilter(dbQuery, query.MediaTypes);
|
||||
dbQuery = ApplyParentFilter(dbQuery, query.ParentId);
|
||||
dbQuery = ApplyUserAccessFilter(dbContext, dbQuery, query.UserId);
|
||||
dbQuery = ApplyUserAccessFilter(dbContext, dbQuery, query);
|
||||
|
||||
// Compute the score in SQL: the ternary translates to a CASE WHEN. CleanName is
|
||||
// the pre-normalized (lowercase, diacritic-stripped) form, so we score against it
|
||||
@@ -196,8 +196,9 @@ public class SqlSearchProvider : IInternalSearchProvider
|
||||
private IQueryable<BaseItemEntity> ApplyUserAccessFilter(
|
||||
JellyfinDbContext dbContext,
|
||||
IQueryable<BaseItemEntity> query,
|
||||
Guid? userId)
|
||||
SearchProviderQuery searchQuery)
|
||||
{
|
||||
var userId = searchQuery.UserId;
|
||||
if (!userId.HasValue || userId.Value.IsEmpty())
|
||||
{
|
||||
return query;
|
||||
@@ -209,8 +210,7 @@ public class SqlSearchProvider : IInternalSearchProvider
|
||||
return query;
|
||||
}
|
||||
|
||||
var accessFilter = new InternalItemsQuery(user);
|
||||
_libraryManager.ConfigureUserAccess(accessFilter, user);
|
||||
var accessFilter = SearchQueryAccessFilter.Build(user, searchQuery, _libraryManager);
|
||||
return _queryHelpers.ApplyAccessFiltering(dbContext, query, accessFilter);
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
|
||||
}
|
||||
|
||||
var candidateRows = await context.ItemValuesMap.AsNoTracking()
|
||||
.Where(m => m.ItemValue.Type == valueType && allKeys.Contains(m.ItemValue.CleanValue))
|
||||
.Where(m => !m.Item.PrimaryVersionId.HasValue && m.ItemValue.Type == valueType && allKeys.Contains(m.ItemValue.CleanValue))
|
||||
.Select(m => new { m.ItemId, Key = m.ItemValue.CleanValue })
|
||||
.ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -276,6 +276,7 @@ public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie
|
||||
if (personSourceRows.Count > 0)
|
||||
{
|
||||
var personCandidateRows = await context.PeopleBaseItemMap.AsNoTracking()
|
||||
.Where(m => !m.Item.PrimaryVersionId.HasValue)
|
||||
.Where(m => context.PeopleBaseItemMap
|
||||
.Where(s => sourceIds.Contains(s.ItemId) && _scoredPersonTypes.Contains(s.People.PersonType))
|
||||
.Select(s => s.PeopleId)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.SimilarItems;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the access filter that decides which items a similar-items lookup may return for a user.
|
||||
/// </summary>
|
||||
internal static class SimilarItemsAccessFilter
|
||||
{
|
||||
private static readonly BaseItemKind[] _itemByNameKinds =
|
||||
[
|
||||
BaseItemKind.Person,
|
||||
BaseItemKind.Genre,
|
||||
BaseItemKind.MusicGenre,
|
||||
BaseItemKind.MusicArtist,
|
||||
BaseItemKind.Studio
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Builds an access filter carrying the user's library access and parental restrictions.
|
||||
/// </summary>
|
||||
/// <param name="user">The user the lookup runs for.</param>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <returns>The access filter.</returns>
|
||||
public static InternalItemsQuery Build(User user, ILibraryManager libraryManager)
|
||||
{
|
||||
// IncludeItemTypes is read only for the by-name exemption here; the caller applies this
|
||||
// filter through ApplyAccessFiltering, which does not translate it into a type restriction.
|
||||
var accessFilter = new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes = _itemByNameKinds
|
||||
};
|
||||
|
||||
// ConfigureUserAccess populates TopParentIds for the libraries the user may open.
|
||||
libraryManager.ConfigureUserAccess(accessFilter, user);
|
||||
|
||||
return accessFilter;
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
@@ -16,11 +18,13 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.SimilarItems;
|
||||
@@ -35,6 +39,8 @@ public class SimilarItemsManager : ISimilarItemsManager
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly IItemQueryHelpers _queryHelpers;
|
||||
private ISimilarItemsProvider[] _similarItemsProviders = [];
|
||||
|
||||
/// <summary>
|
||||
@@ -45,18 +51,24 @@ public class SimilarItemsManager : ISimilarItemsManager
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
/// <param name="serverConfigurationManager">The server configuration manager.</param>
|
||||
/// <param name="dbProvider">The database context factory.</param>
|
||||
/// <param name="queryHelpers">The shared item query helpers.</param>
|
||||
public SimilarItemsManager(
|
||||
ILogger<SimilarItemsManager> logger,
|
||||
IServerApplicationPaths appPaths,
|
||||
ILibraryManager libraryManager,
|
||||
IFileSystem fileSystem,
|
||||
IServerConfigurationManager serverConfigurationManager)
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider,
|
||||
IItemQueryHelpers queryHelpers)
|
||||
{
|
||||
_logger = logger;
|
||||
_appPaths = appPaths;
|
||||
_libraryManager = libraryManager;
|
||||
_fileSystem = fileSystem;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_dbProvider = dbProvider;
|
||||
_queryHelpers = queryHelpers;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -183,6 +195,7 @@ public class SimilarItemsManager : ISimilarItemsManager
|
||||
// Collect references in batches and resolve against local library.
|
||||
// Stop fetching once we have enough resolved local items.
|
||||
const int BatchSize = 20;
|
||||
const int MaxRemoteReferenceFetchLimit = 500;
|
||||
var remaining = requestedLimit - allResults.Count;
|
||||
var collectedReferences = new List<SimilarItemReference>();
|
||||
var pendingBatch = new List<SimilarItemReference>();
|
||||
@@ -199,7 +212,7 @@ public class SimilarItemsManager : ISimilarItemsManager
|
||||
remaining -= resolvedItems.Count;
|
||||
pendingBatch.Clear();
|
||||
|
||||
if (remaining <= 0)
|
||||
if (remaining <= 0 || collectedReferences.Count >= MaxRemoteReferenceFetchLimit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -229,11 +242,64 @@ public class SimilarItemsManager : ISimilarItemsManager
|
||||
}
|
||||
}
|
||||
|
||||
return allResults
|
||||
var ordered = allResults
|
||||
.OrderByDescending(x => x.Score)
|
||||
.Select(x => x.Item)
|
||||
.Take(requestedLimit)
|
||||
.ToList();
|
||||
|
||||
return await FilterByLibraryAccessAsync(ordered, user, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<BaseItem>> FilterByLibraryAccessAsync(
|
||||
IReadOnlyList<BaseItem> candidates,
|
||||
User? user,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (candidates.Count == 0 || user is null)
|
||||
{
|
||||
return candidates;
|
||||
}
|
||||
|
||||
var accessFilter = SimilarItemsAccessFilter.Build(user, _libraryManager);
|
||||
|
||||
// No accessible libraries means nothing to compare against, and an empty TopParentIds set
|
||||
// would disable the filter rather than reject everything.
|
||||
if (accessFilter.TopParentIds.Length == 0)
|
||||
{
|
||||
return candidates;
|
||||
}
|
||||
|
||||
Guid[] candidateIds = [.. candidates.Select(c => c.Id)];
|
||||
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var baseQuery = dbContext.BaseItems
|
||||
.AsNoTracking()
|
||||
.WhereOneOrMany(candidateIds, e => e.Id);
|
||||
|
||||
baseQuery = _queryHelpers.ApplyAccessFiltering(dbContext, baseQuery, accessFilter);
|
||||
|
||||
var allowedCount = await baseQuery.CountAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (allowedCount == candidates.Count)
|
||||
{
|
||||
return candidates;
|
||||
}
|
||||
|
||||
var allowedIds = await baseQuery
|
||||
.Select(e => e.Id)
|
||||
.ToHashSetAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var filtered = candidates.Where(c => allowedIds.Contains(c.Id)).ToList();
|
||||
_logger.LogDebug(
|
||||
"Dropped {Dropped} of {Total} similar-item candidates due to user access filtering",
|
||||
candidates.Count - filtered.Count,
|
||||
candidates.Count);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -375,19 +441,39 @@ public class SimilarItemsManager : ISimilarItemsManager
|
||||
|
||||
var batchResults = await batchProvider.GetBatchSimilarItemsAsync(baselineItems, query, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Filter once across every category rather than per baseline, so a batch provider costs one
|
||||
// access query no matter how many categories it produced.
|
||||
var allItems = batchResults.Values.SelectMany(items => items).DistinctBy(item => item.Id).ToList();
|
||||
var allowed = await FilterByLibraryAccessAsync(allItems, query.User, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
HashSet<Guid>? allowedIds = allowed.Count == allItems.Count
|
||||
? null
|
||||
: [.. allowed.Select(item => item.Id)];
|
||||
|
||||
var recommendations = new List<SimilarItemsRecommendation>(baselineItems.Count);
|
||||
foreach (var baseline in baselineItems)
|
||||
{
|
||||
if (batchResults.TryGetValue(baseline.Id, out var similar) && similar.Count > 0)
|
||||
if (!batchResults.TryGetValue(baseline.Id, out var similar) || similar.Count == 0)
|
||||
{
|
||||
recommendations.Add(new SimilarItemsRecommendation
|
||||
{
|
||||
BaselineItemName = baseline.Name,
|
||||
CategoryId = baseline.Id,
|
||||
RecommendationType = recommendationType,
|
||||
Items = similar
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowedIds is not null)
|
||||
{
|
||||
similar = similar.Where(item => allowedIds.Contains(item.Id)).ToList();
|
||||
if (similar.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
recommendations.Add(new SimilarItemsRecommendation
|
||||
{
|
||||
BaselineItemName = baseline.Name,
|
||||
CategoryId = baseline.Id,
|
||||
RecommendationType = recommendationType,
|
||||
Items = similar
|
||||
});
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
|
||||
@@ -352,6 +352,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <inheritdoc />
|
||||
public UserItemData? GetUserData(User user, BaseItem item)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
var row = ResolveUserDataRow(item, item.UserData?.Where(e => e.UserId.Equals(user.Id)));
|
||||
return row is not null ? Map(row) : new UserItemData()
|
||||
{
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (_config.Configuration.EnableFolderView)
|
||||
{
|
||||
var name = _localizationManager.GetLocalizedString("Folders");
|
||||
var name = _localizationManager.GetServerLocalizedString("Folders");
|
||||
list.Add(_libraryManager.GetNamedView(name, CollectionType.folders, string.Empty));
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName)
|
||||
{
|
||||
var name = _localizationManager.GetLocalizedString(localizationKey);
|
||||
var name = _localizationManager.GetServerLocalizedString(localizationKey);
|
||||
|
||||
return GetUserSubViewWithName(name, parentId, type, sortName);
|
||||
}
|
||||
@@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return GetUserView((Folder)parents[0], viewType, string.Empty);
|
||||
}
|
||||
|
||||
var name = _localizationManager.GetLocalizedString(localizationKey);
|
||||
var name = _localizationManager.GetServerLocalizedString(localizationKey);
|
||||
return _libraryManager.GetNamedView(user, name, viewType, sortName);
|
||||
}
|
||||
|
||||
@@ -396,6 +396,12 @@ namespace Emby.Server.Implementations.Library
|
||||
query.Limit = limit;
|
||||
return _libraryManager.GetLatestItemList(query, parents, CollectionType.movies);
|
||||
}
|
||||
|
||||
if (collectionType is null)
|
||||
{
|
||||
query.Limit = limit;
|
||||
return _libraryManager.GetLatestItemList(query, parents, CollectionType.unknown);
|
||||
}
|
||||
}
|
||||
|
||||
return _libraryManager.GetItemList(query, parents);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -61,6 +62,9 @@ public class ArtistsValidator
|
||||
var count = names.Count;
|
||||
var refreshed = 0;
|
||||
|
||||
var liveIds = new HashSet<Guid>();
|
||||
var unresolved = 0;
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
try
|
||||
@@ -73,13 +77,20 @@ public class ArtistsValidator
|
||||
|
||||
// Fall back to GetArtist if not found (creates new item if needed)
|
||||
item ??= _libraryManager.GetArtist(name);
|
||||
var isNew = !existingArtistIds.Contains(item.Id);
|
||||
var neverRefreshed = item.DateLastRefreshed == default;
|
||||
|
||||
if (isNew || neverRefreshed)
|
||||
// A name with no item is nothing to refresh, and nothing to keep alive either.
|
||||
if (item is not null)
|
||||
{
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
refreshed++;
|
||||
liveIds.Add(item.Id);
|
||||
|
||||
var isNew = !existingArtistIds.Contains(item.Id);
|
||||
var neverRefreshed = item.DateLastRefreshed == default;
|
||||
|
||||
if (isNew || neverRefreshed)
|
||||
{
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
refreshed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -88,6 +99,7 @@ public class ArtistsValidator
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
unresolved++;
|
||||
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
|
||||
}
|
||||
|
||||
@@ -101,13 +113,26 @@ public class ArtistsValidator
|
||||
|
||||
_logger.LogInformation("Refreshed metadata for {RefreshedCount} new artists out of {TotalCount} total", refreshed, count);
|
||||
|
||||
// Every name that threw is a name whose artist is missing from the live set, and deleting against
|
||||
// a live set with holes in it deletes artists the library still refers to. Leave the sweep to a
|
||||
// run that got a clean read of them.
|
||||
if (unresolved > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Not removing dead artists: {Count} of {TotalCount} names could not be resolved this run",
|
||||
unresolved,
|
||||
count);
|
||||
|
||||
progress.Report(100);
|
||||
return;
|
||||
}
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.MusicArtist],
|
||||
IsDeadArtist = true,
|
||||
IsLocked = false
|
||||
}).Cast<MusicArtist>()
|
||||
.Where(item => item.IsAccessedByName)
|
||||
}).OfType<MusicArtist>()
|
||||
.Where(item => item.IsAccessedByName && !liveIds.Contains(item.Id))
|
||||
.ToList();
|
||||
|
||||
foreach (var item in deadEntities)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures top-level library folders have a primary poster after scans.
|
||||
/// Poster extraction is attempted before library scanning. When a library is
|
||||
/// empty at that point, no poster can be extracted. This post-scan task reruns
|
||||
/// metadata extraction for top-level folders that are still missing images.
|
||||
/// </summary>
|
||||
public class CollectionPosterVerifyPostScanTask : ILibraryPostScanTask
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<CollectionPosterVerifyPostScanTask> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CollectionPosterVerifyPostScanTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public CollectionPosterVerifyPostScanTask(
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<CollectionPosterVerifyPostScanTask> logger)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the specified progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var libraries = _libraryManager.GetUserRootFolder().Children.OfType<CollectionFolder>().ToList();
|
||||
var totalLibraries = libraries.Count;
|
||||
var processedLibraries = 0;
|
||||
|
||||
foreach (var library in libraries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!library.HasImage(ImageType.Primary))
|
||||
{
|
||||
_logger.LogDebug("Library {LibraryName} is missing a primary image. Refreshing metadata.", library.Name);
|
||||
await library.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
processedLibraries++;
|
||||
progress.Report((double)processedLibraries / totalLibraries * 100);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Validators;
|
||||
@@ -17,104 +17,143 @@ namespace Emby.Server.Implementations.Library.Validators;
|
||||
public class PeopleValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// The _library manager.
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// The _logger.
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger<PeopleValidator> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PeopleValidator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
|
||||
public PeopleValidator(ILibraryManager libraryManager, ILogger<PeopleValidator> logger)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the people.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
|
||||
// Before the refresh below walks them: a credit no item maps to any more stands for nothing,
|
||||
// and while it is there the person it names cannot reach the dead-person sweep either.
|
||||
var numOrphaned = _libraryManager.DeleteOrphanedCredits();
|
||||
if (numOrphaned > 0)
|
||||
{
|
||||
_logger.LogInformation("Deleted {Amount} credits no item maps to", numOrphaned);
|
||||
}
|
||||
|
||||
var names = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
|
||||
var existingPersonIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Person]
|
||||
}).ToHashSet();
|
||||
|
||||
var (newNames, deadIds) = PartitionCreditsByPersonId(names, _libraryManager.GetPersonId, existingPersonIds);
|
||||
|
||||
var numComplete = 0;
|
||||
var count = names.Count;
|
||||
var refreshed = 0;
|
||||
|
||||
var numPeople = people.Count;
|
||||
|
||||
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
|
||||
|
||||
_logger.LogDebug("Will refresh {Amount} people", numPeople);
|
||||
|
||||
foreach (var person in people)
|
||||
foreach (var name in names)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var item = _libraryManager.GetPerson(person);
|
||||
if (item is null)
|
||||
var item = _libraryManager.GetOrCreatePerson(name);
|
||||
var isNew = !existingPersonIds.Contains(item.Id);
|
||||
var neverRefreshed = item.DateLastRefreshed == default;
|
||||
|
||||
if (isNew || neverRefreshed)
|
||||
{
|
||||
_logger.LogWarning("Failed to get person: {Name}", person);
|
||||
continue;
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
refreshed++;
|
||||
}
|
||||
|
||||
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
|
||||
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
|
||||
};
|
||||
|
||||
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't clutter the log
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error validating IBN entry {Person}", person);
|
||||
_logger.LogError(ex, "Error refreshing {PersonName}", name);
|
||||
}
|
||||
|
||||
// Update progress
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= numPeople;
|
||||
percent /= count;
|
||||
percent *= 100;
|
||||
|
||||
subProgress.Report(100 * percent);
|
||||
progress.Report(percent);
|
||||
}
|
||||
|
||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.Person],
|
||||
IsDeadPerson = true,
|
||||
IsLocked = false
|
||||
});
|
||||
_logger.LogInformation(
|
||||
"Refreshed metadata for {RefreshedCount} people out of {TotalCount} total, {NewCount} of which had no item yet",
|
||||
refreshed,
|
||||
count,
|
||||
newNames.Count);
|
||||
|
||||
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
|
||||
// A person somebody locked is theirs, not ours, however little the library still credits them.
|
||||
var deadEntities = deadIds
|
||||
.Select(_libraryManager.GetItemById)
|
||||
.OfType<Person>()
|
||||
.Where(item => !item.IsLocked)
|
||||
.ToList();
|
||||
|
||||
var i = 0;
|
||||
foreach (var item in deadEntities.Chunk(500))
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
_libraryManager.DeleteItemsUnsafeFast(item, true);
|
||||
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
|
||||
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
||||
}
|
||||
|
||||
_libraryManager.DeleteItemsUnsafeFast(deadEntities, deleteSourceFiles: true);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
_logger.LogInformation("People validation complete");
|
||||
/// <summary>
|
||||
/// Splits the person items into the ones a credit still calls for and the ones nothing does.
|
||||
/// </summary>
|
||||
/// <param name="creditNames">Every name credited on an item, from the people table.</param>
|
||||
/// <param name="getPersonId">Maps a credit name to the id its person item has.</param>
|
||||
/// <param name="existingPersonIds">The ids of the person items that exist.</param>
|
||||
/// <returns>The credits needing an item, and the ids of the items nothing credits.</returns>
|
||||
internal static (List<string> NewNames, List<Guid> DeadIds) PartitionCreditsByPersonId(
|
||||
IReadOnlyList<string> creditNames,
|
||||
Func<string, Guid> getPersonId,
|
||||
IReadOnlySet<Guid> existingPersonIds)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(creditNames);
|
||||
ArgumentNullException.ThrowIfNull(getPersonId);
|
||||
ArgumentNullException.ThrowIfNull(existingPersonIds);
|
||||
|
||||
var newNames = new List<string>();
|
||||
var liveIds = new HashSet<Guid>();
|
||||
|
||||
foreach (var name in creditNames)
|
||||
{
|
||||
var personId = getPersonId(name);
|
||||
|
||||
// Distinct credit names can normalize onto one id; only the first of them needs an item.
|
||||
if (liveIds.Add(personId) && !existingPersonIds.Contains(personId))
|
||||
{
|
||||
newNames.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
var deadIds = existingPersonIds.Where(id => !liveIds.Contains(id)).ToList();
|
||||
|
||||
return (newNames, deadIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
"TaskAudioNormalization": "تطبيع الصوت",
|
||||
"TaskAudioNormalizationDescription": "يفحص الملفات لجمع بيانات تطبيع الصوت.",
|
||||
"TaskDownloadMissingLyrics": "تنزيل الكلمات المفقودة",
|
||||
"TaskDownloadMissingLyricsDescription": "ينزّل الكلمات للأغاني.",
|
||||
"TaskDownloadMissingLyricsDescription": "تحميل كلمات الأغاني",
|
||||
"TaskExtractMediaSegments": "فحص مقاطع المحتوى",
|
||||
"TaskExtractMediaSegmentsDescription": "يستخرج أو يحصل على مقاطع المحتوى من الملحقات المفعّلة لمقاطع المحتوى (MediaSegment).",
|
||||
"TaskMoveTrickplayImages": "نقل موقع صور معاينات التنقل",
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
|
||||
"CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل.",
|
||||
"Original": "فريد",
|
||||
"LyricDownloadFailureFromForItem": "فشل تحميل الكلمات من {0} إلى {1}"
|
||||
"LyricDownloadFailureFromForItem": "فشل تحميل الكلمات من {0} إلى {1}",
|
||||
"NameExtraBehindTheScenes": "خلف المشاهد",
|
||||
"NameExtraClip": "مقطع",
|
||||
"NameExtraDeletedScene": "المشهد المحذوف",
|
||||
"NameExtraInterview": "مقابلة",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "عيّنة",
|
||||
"NameExtraScene": "مشهد",
|
||||
"NameExtraShort": "قصير",
|
||||
"NameExtraThemeSong": "الاغنية السمة",
|
||||
"NameExtraThemeVideo": "الفيديو السمة",
|
||||
"NameExtraFeaturette": "فيلم قصير إضافي",
|
||||
"NameExtraTrailer": "إعلان ترويجي",
|
||||
"NameExtraUnknown": "إضافي"
|
||||
}
|
||||
|
||||
@@ -106,5 +106,17 @@
|
||||
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
|
||||
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
|
||||
"CleanupUserDataTask": "Задача па ачыстцы даных карыстальніка",
|
||||
"CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
|
||||
"CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён.",
|
||||
"LyricDownloadFailureFromForItem": "Не ўдалося загрузіць тэкст песні з {0} для {1}",
|
||||
"NameExtraDeletedScene": "Выдаленая сцэна",
|
||||
"NameExtraInterview": "Інтэрв'ю",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraScene": "Сцэна",
|
||||
"NameExtraTrailer": "Трэйлер",
|
||||
"NameExtraBehindTheScenes": "За кулісамі",
|
||||
"NameExtraClip": "Кліп",
|
||||
"NameExtraFeaturette": "Кароткаметражка",
|
||||
"NameExtraSample": "Прыклад",
|
||||
"NameExtraShort": "Кароткаметражка",
|
||||
"NameExtraThemeSong": "Тэматычная песня"
|
||||
}
|
||||
|
||||
@@ -106,5 +106,20 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.",
|
||||
"TaskExtractMediaSegments": "Сканиране за сегменти",
|
||||
"CleanupUserDataTask": "Задача за почистване на потребителски данни",
|
||||
"CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни."
|
||||
"CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни.",
|
||||
"LyricDownloadFailureFromForItem": "Текстът на песента не успя да се изтегли от {0} за {1}",
|
||||
"NameExtraBehindTheScenes": "Зад кулисите",
|
||||
"NameExtraScene": "Сцена",
|
||||
"NameExtraShort": "Откъс",
|
||||
"NameExtraThemeVideo": "Тематично видео",
|
||||
"NameExtraTrailer": "Трейлър",
|
||||
"NameExtraUnknown": "Екстра",
|
||||
"NameExtraClip": "Клип",
|
||||
"NameExtraDeletedScene": "Изтрита Сцена",
|
||||
"NameExtraFeaturette": "Кратък филм",
|
||||
"NameExtraInterview": "Интервю",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Пример",
|
||||
"NameExtraThemeSong": "Тема-песен",
|
||||
"Original": "Оригинал"
|
||||
}
|
||||
|
||||
@@ -106,5 +106,17 @@
|
||||
"TaskMoveTrickplayImages": "Migracija lokacije slike Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke trik-igara prema postavkama biblioteke.",
|
||||
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
|
||||
"CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana."
|
||||
"CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana.",
|
||||
"NameExtraBehindTheScenes": "Iza kulisa",
|
||||
"NameExtraClip": "Isječak",
|
||||
"NameExtraDeletedScene": "Izbrišana scena",
|
||||
"NameExtraFeaturette": "Kratki prilog",
|
||||
"NameExtraInterview": "Intervju",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Uzorak",
|
||||
"NameExtraScene": "Scena",
|
||||
"NameExtraShort": "Kratko",
|
||||
"NameExtraThemeSong": "Tema",
|
||||
"NameExtraThemeVideo": "Tematski video",
|
||||
"NameExtraTrailer": "Najava"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
|
||||
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari",
|
||||
"Original": "Original",
|
||||
"LyricDownloadFailureFromForItem": "No s'han pogut descarregar les lletres des de {0} per a {1}"
|
||||
"LyricDownloadFailureFromForItem": "No s'han pogut descarregar les lletres des de {0} per a {1}",
|
||||
"NameExtraBehindTheScenes": "Rere les càmeres",
|
||||
"NameExtraClip": "Tall",
|
||||
"NameExtraDeletedScene": "Escena eliminada",
|
||||
"NameExtraFeaturette": "Migmetratge",
|
||||
"NameExtraInterview": "Entrevista",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Mostra",
|
||||
"NameExtraScene": "Escena",
|
||||
"NameExtraShort": "Curt",
|
||||
"NameExtraThemeSong": "Tema musical",
|
||||
"NameExtraThemeVideo": "Vídeo temàtic",
|
||||
"NameExtraTrailer": "Tràiler",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.",
|
||||
"CleanupUserDataTask": "Pročistit uživatelská data",
|
||||
"Original": "Originál",
|
||||
"LyricDownloadFailureFromForItem": "Nepodařilo se stáhnout texty pro {1} ze služby {0}"
|
||||
"LyricDownloadFailureFromForItem": "Nepodařilo se stáhnout texty pro {1} ze služby {0}",
|
||||
"NameExtraBehindTheScenes": "Zákulisí",
|
||||
"NameExtraClip": "Klip",
|
||||
"NameExtraDeletedScene": "Vymazaná scéna",
|
||||
"NameExtraFeaturette": "Featurette",
|
||||
"NameExtraInterview": "Rozhovor",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Ukázka",
|
||||
"NameExtraScene": "Scéna",
|
||||
"NameExtraShort": "Krátké",
|
||||
"NameExtraThemeSong": "Úvodní píseň",
|
||||
"NameExtraThemeVideo": "Úvodní video",
|
||||
"NameExtraTrailer": "Upoutávka",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"Shows": "Serier",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.",
|
||||
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
|
||||
"TvShows": "TV-serier",
|
||||
"TvShows": "Serier",
|
||||
"UserCreatedWithName": "Bruger {0} er blevet oprettet",
|
||||
"UserDeletedWithName": "Brugeren {0} er nu slettet",
|
||||
"UserDownloadingItemWithValues": "{0} henter {1}",
|
||||
@@ -80,8 +80,8 @@
|
||||
"TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
|
||||
"TaskRefreshChannelsDescription": "Opdaterer information for internetkanaler.",
|
||||
"TaskRefreshChannels": "Opdatér kanaler",
|
||||
"TaskCleanTranscodeDescription": "Fjerner omkodningsfiler, som er mere end 1 dag gamle.",
|
||||
"TaskCleanTranscode": "Tøm omkodningsmappen",
|
||||
"TaskCleanTranscodeDescription": "Fjerner transkodningsfiler, som er mere end 1 dag gamle.",
|
||||
"TaskCleanTranscode": "Tøm transkodningsmappen",
|
||||
"TaskRefreshPeople": "Opdatér personer",
|
||||
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
|
||||
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "Brugerdata oprydningsopgave",
|
||||
"CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage.",
|
||||
"LyricDownloadFailureFromForItem": "Sangtekster kunne ikke downloades fra {0} til {1}",
|
||||
"Original": "Original"
|
||||
"Original": "Original",
|
||||
"NameExtraBehindTheScenes": "Bag scenerne",
|
||||
"NameExtraClip": "Klip",
|
||||
"NameExtraDeletedScene": "Slettet scene",
|
||||
"NameExtraFeaturette": "Featurette",
|
||||
"NameExtraInterview": "Interview",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Smagsprøve",
|
||||
"NameExtraScene": "Scene",
|
||||
"NameExtraShort": "Kort",
|
||||
"NameExtraThemeSong": "Temasang",
|
||||
"NameExtraThemeVideo": "Temavideo",
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraUnknown": "Ekstra"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"AppDeviceValues": "App: {0}, Gerät: {1}",
|
||||
"Artists": "Interpreten",
|
||||
"Artists": "Künstler",
|
||||
"AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
|
||||
"Books": "Bücher",
|
||||
"ChapterNameValue": "Kapitel {0}",
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
|
||||
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind.",
|
||||
"Original": "Original",
|
||||
"LyricDownloadFailureFromForItem": "Fehler beim Download der Songtexte von {0} für {1}"
|
||||
"LyricDownloadFailureFromForItem": "Fehler beim Download der Songtexte von {0} für {1}",
|
||||
"NameExtraBehindTheScenes": "Behind The Scenes",
|
||||
"NameExtraDeletedScene": "Entfernte Szene",
|
||||
"NameExtraInterview": "Interview",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Ausschnitt",
|
||||
"NameExtraScene": "Szene",
|
||||
"NameExtraShort": "Kurzfilm",
|
||||
"NameExtraThemeSong": "Titellied",
|
||||
"NameExtraThemeVideo": "Titelvideo",
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraFeaturette": "Hinter den Kulissen"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Καθαρίζει όλα τα δεδομένα χρήστη (κατάσταση παρακολούθησης, κατάσταση αγαπημένων κ.λπ.) από πολυμέσα που δεν υπάρχουν πλέον για τουλάχιστον 90 ημέρες.",
|
||||
"CleanupUserDataTask": "Εργασία εκκαθάρισης δεδομένων χρήστη",
|
||||
"LyricDownloadFailureFromForItem": "Αποτυχία λήψης στίχων από {0} για {1}",
|
||||
"Original": "Πρωτότυπο"
|
||||
"Original": "Πρωτότυπο",
|
||||
"NameExtraBehindTheScenes": "Πίσω από τις Σκηνές",
|
||||
"NameExtraDeletedScene": "Διεγραμμένη Σκηνή",
|
||||
"NameExtraFeaturette": "Πρόσθετα βίντεο",
|
||||
"NameExtraInterview": "Συνέντευξη",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Δείγμα",
|
||||
"NameExtraScene": "Σκηνή",
|
||||
"NameExtraShort": "Βίντεο μικρού μήκους",
|
||||
"NameExtraThemeSong": "Θεματικό Τραγούδι",
|
||||
"NameExtraThemeVideo": "Θεματικό Βίντεο",
|
||||
"NameExtraTrailer": "τρέιλερ ταινίας",
|
||||
"NameExtraUnknown": "Πρόσθετα",
|
||||
"NameExtraClip": "Απόσπασμα"
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"Music": "Music",
|
||||
"MusicVideos": "Music Videos",
|
||||
"NameInstallFailed": "{0} installation failed",
|
||||
"NameSeasonNumber": "Season {0}",
|
||||
"NameSeasonUnknown": "Season Unknown",
|
||||
"NameSeasonNumber": "Series {0}",
|
||||
"NameSeasonUnknown": "Series Unknown",
|
||||
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Application update available",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "User data cleanup task",
|
||||
"CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favourite status etc) from media that is no longer present for at least 90 days.",
|
||||
"LyricDownloadFailureFromForItem": "Lyrics failed to download from {0} for {1}",
|
||||
"Original": "Original"
|
||||
"Original": "Original",
|
||||
"NameExtraBehindTheScenes": "Behind The Scenes",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Deleted Scene",
|
||||
"NameExtraFeaturette": "Featurette",
|
||||
"NameExtraInterview": "Interview",
|
||||
"NameExtraSample": "Sample",
|
||||
"NameExtraScene": "Scene",
|
||||
"NameExtraShort": "Short",
|
||||
"NameExtraThemeSong": "Theme Song",
|
||||
"NameExtraThemeVideo": "Theme Video",
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameExtraNumbered": "{0} {1}"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,19 @@
|
||||
"Movies": "Movies",
|
||||
"Music": "Music",
|
||||
"MusicVideos": "Music Videos",
|
||||
"NameExtraBehindTheScenes": "Behind The Scenes",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Deleted Scene",
|
||||
"NameExtraFeaturette": "Featurette",
|
||||
"NameExtraInterview": "Interview",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Sample",
|
||||
"NameExtraScene": "Scene",
|
||||
"NameExtraShort": "Short",
|
||||
"NameExtraThemeSong": "Theme Song",
|
||||
"NameExtraThemeVideo": "Theme Video",
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameInstallFailed": "{0} installation failed",
|
||||
"NameSeasonNumber": "Season {0}",
|
||||
"NameSeasonUnknown": "Season Unknown",
|
||||
|
||||
@@ -97,5 +97,9 @@
|
||||
"TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.",
|
||||
"TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)",
|
||||
"TaskAudioNormalization": "Normaligo Sonnivela",
|
||||
"HearingImpaired": "Surda"
|
||||
"HearingImpaired": "Surda",
|
||||
"NameExtraDeletedScene": "Forigita sceno",
|
||||
"NameExtraScene": "Sceno",
|
||||
"NameExtraThemeSong": "Tema Kanto",
|
||||
"Original": "Originala"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, estado de los favoritos, etc.) que no están presentes en la biblioteca por al menos 90 días.",
|
||||
"CleanupUserDataTask": "Tarea de limpieza de datos de usuarios",
|
||||
"LyricDownloadFailureFromForItem": "No se pudo descargar la letra desde {0} para {1}",
|
||||
"Original": "Original"
|
||||
"Original": "Original",
|
||||
"NameExtraBehindTheScenes": "Detrás de cámaras",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Escena eliminada",
|
||||
"NameExtraFeaturette": "Minidocumental",
|
||||
"NameExtraInterview": "Entrevista",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Muestra",
|
||||
"NameExtraScene": "Escena",
|
||||
"NameExtraShort": "Cortometraje",
|
||||
"NameExtraThemeSong": "Música de presentación",
|
||||
"NameExtraThemeVideo": "Video de presentación",
|
||||
"NameExtraTrailer": "Tráiler",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -106,5 +106,20 @@
|
||||
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
|
||||
"CleanupUserDataTask": "Tarea de limpieza de los datos del usuario",
|
||||
"CleanupUserDataTaskDescription": "Limpia toda la información de usuario (Estado de última vez visto, favoritos, etc) del archivo media que no está presente por los últimos 90 días."
|
||||
"CleanupUserDataTaskDescription": "Limpia toda la información de usuario (Estado de última vez visto, favoritos, etc) del archivo media que no está presente por los últimos 90 días.",
|
||||
"LyricDownloadFailureFromForItem": "No se pudo descargar la letra desde {0} para {1}",
|
||||
"NameExtraBehindTheScenes": "Detrás de cámaras",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Escena eliminada",
|
||||
"NameExtraFeaturette": "Minidocumental",
|
||||
"NameExtraInterview": "Entrevista",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Muestra",
|
||||
"NameExtraScene": "Escena",
|
||||
"NameExtraShort": "Cortometraje",
|
||||
"NameExtraThemeSong": "Música de presentación",
|
||||
"NameExtraThemeVideo": "Video de presentación",
|
||||
"NameExtraTrailer": "Tráiler",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"Original": "Original"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "Tarea de limpieza de datos del usuario",
|
||||
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días.",
|
||||
"Original": "Original",
|
||||
"LyricDownloadFailureFromForItem": "No se pudieron descargar las letras desde {0} para {1}"
|
||||
"LyricDownloadFailureFromForItem": "No se pudieron descargar las letras desde {0} para {1}",
|
||||
"NameExtraBehindTheScenes": "Detrás de Cámaras",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Escena eliminada",
|
||||
"NameExtraFeaturette": "Reportaje especial",
|
||||
"NameExtraInterview": "Entrevista",
|
||||
"NameExtraSample": "Muestra",
|
||||
"NameExtraScene": "Escena",
|
||||
"NameExtraShort": "Cortometraje",
|
||||
"NameExtraThemeSong": "Tema principal",
|
||||
"NameExtraThemeVideo": "Vídeo del tema principal",
|
||||
"NameExtraTrailer": "Tráiler",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameExtraNumbered": "{0} {1}"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "Tarea de limpieza de datos de usuario",
|
||||
"CleanupUserDataTaskDescription": "Limpia todos los datos de usuario (estado de visualización, favoritos, etc.) que no están presentes en la biblioteca por al menos 90 días.",
|
||||
"LyricDownloadFailureFromForItem": "No se pudo descargar las letras de {0} para {1}",
|
||||
"Original": "Original"
|
||||
"Original": "Original",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameExtraBehindTheScenes": "Detrás de cámaras",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Escena eliminada",
|
||||
"NameExtraFeaturette": "Minidocumental",
|
||||
"NameExtraInterview": "Entrevista",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Muestra",
|
||||
"NameExtraScene": "Escena",
|
||||
"NameExtraShort": "Cortometraje",
|
||||
"NameExtraThemeSong": "Música de presentación",
|
||||
"NameExtraThemeVideo": "Video de presentación",
|
||||
"NameExtraTrailer": "Tráiler"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "Puhasta kasutajaandmed",
|
||||
"CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud.",
|
||||
"LyricDownloadFailureFromForItem": "Laulusõnade hankimine teenusest {0} loole {1} nurjus",
|
||||
"Original": "Algne"
|
||||
"Original": "Algne",
|
||||
"NameExtraBehindTheScenes": "Kulisside taga",
|
||||
"NameExtraDeletedScene": "Väljajäetud stseen",
|
||||
"NameExtraFeaturette": "Lisalõik",
|
||||
"NameExtraInterview": "Intervjuu",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Näidis",
|
||||
"NameExtraScene": "Stseen",
|
||||
"NameExtraShort": "Lühifilm",
|
||||
"NameExtraThemeSong": "Tunnusmeloodia",
|
||||
"NameExtraThemeVideo": "Tunnusvideo",
|
||||
"NameExtraTrailer": "Treiler",
|
||||
"NameExtraUnknown": "Lisamaterjal",
|
||||
"NameExtraClip": "Videoklipp"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Gutxienez 90 egunez dagoeneko existitzen ez den multimediatik erabiltzaile-datu guztiak (ikusteko egoera, gogokoen egoera, etab.) garbitzen ditu.",
|
||||
"CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina",
|
||||
"LyricDownloadFailureFromForItem": "Ezin izan dira {1}-ren letrak deskargatu {0}-tik",
|
||||
"Original": "Jatorrizkoa"
|
||||
"Original": "Jatorrizkoa",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Lagina",
|
||||
"NameExtraScene": "Eszena",
|
||||
"NameExtraShort": "Laburra",
|
||||
"NameExtraThemeSong": "Gai-abestia",
|
||||
"NameExtraThemeVideo": "Gai-bideoa",
|
||||
"NameExtraBehindTheScenes": "Eszenen atzean",
|
||||
"NameExtraClip": "Klipa",
|
||||
"NameExtraDeletedScene": "Ezabatutako eszena",
|
||||
"NameExtraFeaturette": "Erreportajea",
|
||||
"NameExtraInterview": "Elkarrizketa",
|
||||
"NameExtraTrailer": "Trailerra",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,17 @@
|
||||
"CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä",
|
||||
"CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään.",
|
||||
"LyricDownloadFailureFromForItem": "Sanoitusten lataus kohteesta {0} kappaleelle {1} epäonnistui",
|
||||
"Original": "Alkuperäinen"
|
||||
"Original": "Alkuperäinen",
|
||||
"NameExtraBehindTheScenes": "Kulissien Takana",
|
||||
"NameExtraClip": "Klippi",
|
||||
"NameExtraDeletedScene": "Poistettu Kohtaus",
|
||||
"NameExtraFeaturette": "Lyhytelokuva",
|
||||
"NameExtraInterview": "Haastattelu",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Näyte",
|
||||
"NameExtraScene": "Kohtaus",
|
||||
"NameExtraShort": "Lyhytfilmi",
|
||||
"NameExtraThemeSong": "Tunnusmusiikki",
|
||||
"NameExtraThemeVideo": "Tunnusvideo",
|
||||
"NameExtraTrailer": "Traileri"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"Artists": "Listafólk",
|
||||
"Collections": "Søvn",
|
||||
"Artists": "Tónlistafólk",
|
||||
"Collections": "Samlingar",
|
||||
"Default": "Forsett",
|
||||
"External": "Ytri",
|
||||
"Genres": "Greinar",
|
||||
"AppDeviceValues": "App: {0}, Eind: {1}",
|
||||
"Books": "Bøkur",
|
||||
"ChapterNameValue": "Kapittul {0}",
|
||||
"Favorites": "Yndis",
|
||||
"Folders": "Mappur",
|
||||
"Favorites": "Yndislista",
|
||||
"Folders": "Skjáttur",
|
||||
"Forced": "Kravt",
|
||||
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
|
||||
"HeaderFavoriteEpisodes": "Yndispartar",
|
||||
"LabelIpAddressValue": "IP-atsetur: {0}",
|
||||
"AuthenticationSucceededWithUserName": "{0} varð samgildur",
|
||||
"AuthenticationSucceededWithUserName": "{0} var samgildur",
|
||||
"HeaderFavoriteShows": "Yndisrøðir",
|
||||
"HeaderLiveTV": "Beinleiðis sjónvarp",
|
||||
"HearingImpaired": "Hoyrnarveik",
|
||||
@@ -29,9 +29,9 @@
|
||||
"NotificationOptionPluginUpdateInstalled": "Ískoytisforrit dagført",
|
||||
"NotificationOptionUserLockedOut": "Brúkari útihýstur",
|
||||
"Photos": "Ljósmyndir",
|
||||
"PluginInstalledWithName": "{0} var innlagt",
|
||||
"PluginUninstalledWithName": "{0} var strikað",
|
||||
"PluginUpdatedWithName": "{0} varð dagført",
|
||||
"PluginInstalledWithName": "{0} innlagt",
|
||||
"PluginUninstalledWithName": "{0} strikað",
|
||||
"PluginUpdatedWithName": "{0} dagført",
|
||||
"Shows": "Røðir",
|
||||
"SubtitleDownloadFailureFromForItem": "Miseydnaðist at niðurtakað undirtekstir til {1} frá {0}",
|
||||
"TvShows": "Sjónvarpsrøðir",
|
||||
@@ -48,5 +48,78 @@
|
||||
"TaskDownloadMissingLyricsDescription": "Niðurtekur sangtekstir",
|
||||
"Movies": "Filmar",
|
||||
"MixedContent": "Blandað innihald",
|
||||
"Music": "Tónleikur"
|
||||
"Music": "Tónleikur",
|
||||
"UserStartedPlayingItemWithValues": "{0} spælur {1} á {2}",
|
||||
"HeaderContinueWatching": "Hald áfram at hyggja",
|
||||
"MusicVideos": "Sjónbandaløg",
|
||||
"TaskUpdatePluginsDescription": "Niðurtekur og innleggur dagføringar til ískoytisforrit ið eru stillaði til at dagførast sjálvvirkandi.",
|
||||
"TaskCleanTranscodeDescription": "Strikar umkotaðar fílar ið eru eldri enn 1 dag.",
|
||||
"TaskOptimizeDatabase": "Albøt dátugrunn",
|
||||
"NameSeasonNumber": "Umfar {0}",
|
||||
"NameSeasonUnknown": "Ókent umfar",
|
||||
"ScheduledTaskFailedWithName": "{0} miseydnaðist",
|
||||
"Undefined": "Óskilmarkað",
|
||||
"TasksMaintenanceCategory": "Viðlíkahald",
|
||||
"TaskCleanLogs": "Reinsa gerðalistaskjáttu",
|
||||
"UserOnlineFromDevice": "{0} er íbundin frá {1}",
|
||||
"HeaderNextUp": "Næst á skránni",
|
||||
"NotificationOptionPluginError": "Brek í ískoytisforriti",
|
||||
"NotificationOptionInstallationFailed": "Innleggingarbrek",
|
||||
"NotificationOptionServerRestartRequired": "Tørvur er á ambætaraendurbyrjan",
|
||||
"TasksApplicationCategory": "Nýtsluskipan",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Skipanardagføring er tøk",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Skipanardagføring var innløgd",
|
||||
"UserStoppedPlayingItemWithValues": "{0} er liðugur at spæla {1} á {2}",
|
||||
"HomeVideos": "Heimaupptøkur",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin-ambætarin er undir byrjanarinnlesing. Vinaliga royn aftur um eitt bil.",
|
||||
"UserOfflineFromDevice": "{0} breyt av á {1}",
|
||||
"UserPasswordChangedWithName": "Loyniorðið hjá brúkaranum {0} er broytt",
|
||||
"TasksChannelsCategory": "Alnetsrásir",
|
||||
"TaskCleanActivityLog": "Reinsa virksemisskrá",
|
||||
"TaskCleanActivityLogDescription": "Strikar skrásetingar eldri enn ásetta aldur.",
|
||||
"TaskCleanCache": "Reinsa kovaskjáttu",
|
||||
"TaskCleanCacheDescription": "Strikar kovafílar ið kervið ikki hevur tørv á longur.",
|
||||
"TaskCleanTranscode": "Reinsa umkotuskjáttu",
|
||||
"TaskDownloadMissingLyrics": "Niðurtak vantandi sangtekstir",
|
||||
"TaskDownloadMissingSubtitles": "Niðurtak vantandi undirtekstir",
|
||||
"CleanupUserDataTaskDescription": "Strikar allar brúkaradátur, so sum spælistøðu, yndislistastøðu o.s.fr., fyri miðlar ið ikki hava verið tøkir í í minsta lagi 90 dagar.",
|
||||
"CleanupUserDataTask": "Koyrsla ið reinsar brúkaradátur",
|
||||
"TaskRefreshPeople": "Dagfør persónsupplýsingar",
|
||||
"TaskRefreshPeopleDescription": "Dagførur metadátur um leikarar og leikstjórar í tínum margmiðlasavni.",
|
||||
"TaskRefreshChannelsDescription": "Dagførur upplýsingar um alnetsrásir.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Leitar á alnótini eftir vantandi undirtekstum grundað á metadátauppsetan.",
|
||||
"NotificationOptionTaskFailed": "Brek undir fyriskipaðari koyrslu",
|
||||
"TaskRefreshLibraryDescription": "Skannar títt miðlasavn fyri nýggjum fílum og dagførur metadátur.",
|
||||
"TaskKeyframeExtractor": "Lyklamyndaúttøka",
|
||||
"TaskKeyframeExtractorDescription": "Úttekur lyklamyndir frá kykmynda-fílum til tess at byggja nágreiniligari HLS-spælilistar. Koyrslan kann taka langa tíð.",
|
||||
"TaskOptimizeDatabaseDescription": "Trýstur dátugrunninin saman og loysur tóma goymslu. Koyrslan kann bøta um avrikið, eftir skanning ella aðrar broytingar í savninum ið elva til dátugrunnsbroytingar.",
|
||||
"TaskRefreshChapterImagesDescription": "Ger smámyndir fyri kykmyndir ið hava kapitlar.",
|
||||
"TaskRefreshChapterImages": "Kapitlamyndaúttøkur",
|
||||
"NotificationOptionVideoPlayback": "Kykmyndaspæl byrjað",
|
||||
"NotificationOptionVideoPlaybackStopped": "Kykmyndaspæl steðgað",
|
||||
"NotificationOptionAudioPlayback": "Ljóðspæl byrjað",
|
||||
"NotificationOptionAudioPlaybackStopped": "Ljóðspæl steðgað",
|
||||
"TaskExtractMediaSegments": "Leita eftir margmiðlabrotum",
|
||||
"TaskExtractMediaSegmentsDescription": "Framleiður upplýsingar um brot í margmiðlum, við hjálp frá MediaSegment-virktum ískoytisforritum.",
|
||||
"NotificationOptionCameraImageUploaded": "Ljósmynd uppsend",
|
||||
"NameExtraShort": "Stuttfilmur",
|
||||
"NameExtraThemeSong": "Eyðkennislag",
|
||||
"NameExtraTrailer": "Brellbiti",
|
||||
"NameExtraInterview": "Samrøða",
|
||||
"NameExtraBehindTheScenes": "Aftanfyri leiktjøldini",
|
||||
"NameExtraClip": "Klipp",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraFeaturette": "Stuttur heimildarfilmur",
|
||||
"TaskAudioNormalization": "Ljóðjavnan",
|
||||
"TaskAudioNormalizationDescription": "Kannar fílur fyri dátum til ljóðjavnan.",
|
||||
"NameExtraSample": "Sýnislutur",
|
||||
"TaskRefreshTrickplayImages": "Framleið Trickplay-myndir",
|
||||
"TaskRefreshTrickplayImagesDescription": "Framleiðir trickplay-myndir fyri kykmyndir í søvnunm har tað er virkt.",
|
||||
"TaskMoveTrickplayImages": "Flyt Trickplay-myndagoymslustað",
|
||||
"TaskMoveTrickplayImagesDescription": "Flytur verandi trickplay-fílur sambært savnsstillingunum.",
|
||||
"NameExtraThemeVideo": "Eyðkenniskykmynd",
|
||||
"NameExtraDeletedScene": "Úrtikin mynd",
|
||||
"NameExtraScene": "Mynd",
|
||||
"NameExtraUnknown": "Eykatilfar",
|
||||
"Original": "Upprunalig(t/ur)"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
|
||||
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
|
||||
"LyricDownloadFailureFromForItem": "Le téléchargement des paroles a échoué de {0} pour {1}",
|
||||
"Original": "Original"
|
||||
"Original": "Original",
|
||||
"NameExtraBehindTheScenes": "Dans Les Coulisses",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Scène supprimée",
|
||||
"NameExtraFeaturette": "Court-métrage",
|
||||
"NameExtraInterview": "Entrevue",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Échantillon",
|
||||
"NameExtraScene": "Scène",
|
||||
"NameExtraShort": "Court-métrage",
|
||||
"NameExtraThemeSong": "Chanson thème",
|
||||
"NameExtraThemeVideo": "Générique",
|
||||
"NameExtraTrailer": "Bande-annonce",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
|
||||
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
|
||||
"LyricDownloadFailureFromForItem": "Le téléchargement des paroles à échoué de {0} pour {1}",
|
||||
"Original": "Original"
|
||||
"Original": "Original",
|
||||
"NameExtraBehindTheScenes": "Dans Les Coulisses",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Scène supprimée",
|
||||
"NameExtraFeaturette": "Court-métrage",
|
||||
"NameExtraInterview": "Entrevue",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Échantillon",
|
||||
"NameExtraScene": "Scène",
|
||||
"NameExtraShort": "Court-métrage",
|
||||
"NameExtraThemeSong": "Thème musical",
|
||||
"NameExtraThemeVideo": "Générique",
|
||||
"NameExtraTrailer": "Bande-annonce",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,17 @@
|
||||
"CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora",
|
||||
"CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad.",
|
||||
"Original": "Bunaidh",
|
||||
"LyricDownloadFailureFromForItem": "Theip ar liricí a íoslódáil ó {0} do {1}"
|
||||
"LyricDownloadFailureFromForItem": "Theip ar liricí a íoslódáil ó {0} do {1}",
|
||||
"NameExtraBehindTheScenes": "Taobh thiar de na Radhairc",
|
||||
"NameExtraClip": "Gearrthóg",
|
||||
"NameExtraDeletedScene": "Radharc Scriosta",
|
||||
"NameExtraFeaturette": "Mionghné",
|
||||
"NameExtraInterview": "Agallamh",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Sampla",
|
||||
"NameExtraScene": "Radharc",
|
||||
"NameExtraShort": "Gearr",
|
||||
"NameExtraThemeSong": "Amhrán Téama",
|
||||
"NameExtraThemeVideo": "Físeán Téama",
|
||||
"NameExtraTrailer": "Leantóir"
|
||||
}
|
||||
|
||||
@@ -107,5 +107,19 @@
|
||||
"TaskAudioNormalizationDescription": "Escanea ficheiros á procura de datos de normalización de volume.",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de datos dos usuarios",
|
||||
"CleanupUserDataTaskDescription": "Limpa todos os datos do usuario (estado de visualización, de favorito etc.) dos medios ausentes polo menos 90 días.",
|
||||
"Original": "Orixinal"
|
||||
"Original": "Orixinal",
|
||||
"LyricDownloadFailureFromForItem": "Non se puideron descargar as letras desde {0} para {1}",
|
||||
"NameExtraBehindTheScenes": "Detrás das Cámaras",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Escena Eliminada",
|
||||
"NameExtraFeaturette": "Reportaxe especial",
|
||||
"NameExtraInterview": "Entrevista",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Mostra",
|
||||
"NameExtraScene": "Escena",
|
||||
"NameExtraShort": "Curtametraxe",
|
||||
"NameExtraThemeSong": "Canción principal",
|
||||
"NameExtraThemeVideo": "Vídeo da canción principal",
|
||||
"NameExtraTrailer": "Tráiler",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,12 @@
|
||||
"CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
|
||||
"CleanupUserDataTask": "משימת ניקוי מידע משתמש",
|
||||
"LyricDownloadFailureFromForItem": "הורדת המילים מ-{0} עבור {1} נכשלה",
|
||||
"Original": "מקור"
|
||||
"Original": "מקור",
|
||||
"NameExtraBehindTheScenes": "מאחורי הקלעים",
|
||||
"NameExtraClip": "קליפ",
|
||||
"NameExtraDeletedScene": "סצנה שנמחקה",
|
||||
"NameExtraFeaturette": "סרט קצר",
|
||||
"NameExtraInterview": "ריאיון",
|
||||
"NameExtraSample": "דגימה",
|
||||
"NameExtraScene": "סצנה"
|
||||
}
|
||||
|
||||
@@ -105,5 +105,21 @@
|
||||
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
|
||||
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
|
||||
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
|
||||
"CleanupUserDataTask": "यूज़र डेटा सफाई कार्य"
|
||||
"CleanupUserDataTask": "यूज़र डेटा सफाई कार्य",
|
||||
"Original": "असली",
|
||||
"LyricDownloadFailureFromForItem": "{0} के लिए {1} से बोल (Lyrics) डाउनलोड करने में विफल रहा",
|
||||
"NameExtraBehindTheScenes": "परदे के पीछे",
|
||||
"NameExtraClip": "क्लिप",
|
||||
"NameExtraDeletedScene": "हटाया गया दृश्य",
|
||||
"NameExtraFeaturette": "फीचरेट",
|
||||
"NameExtraInterview": "साक्षात्कार",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "नमूना",
|
||||
"NameExtraScene": "दृश्य",
|
||||
"NameExtraShort": "शॉर्ट",
|
||||
"NameExtraThemeSong": "थीम सॉन्ग",
|
||||
"NameExtraThemeVideo": "थीम वीडियो",
|
||||
"NameExtraTrailer": "ट्रेलर",
|
||||
"NameExtraUnknown": "अतिरिक्त",
|
||||
"CleanupUserDataTaskDescription": "कम से कम 90 दिनों से अनुपस्थित मीडिया से सभी उपयोगकर्ता डेटा (देखने की स्थिति, पसंदीदा स्थिति आदि) को साफ़ करता है।"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,17 @@
|
||||
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
|
||||
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana.",
|
||||
"Original": "Original",
|
||||
"LyricDownloadFailureFromForItem": "Preuzimanje tekstova pjesmi od {0} za {1} nije uspjelo"
|
||||
"LyricDownloadFailureFromForItem": "Preuzimanje tekstova pjesmi od {0} za {1} nije uspjelo",
|
||||
"NameExtraBehindTheScenes": "Iza kulisa",
|
||||
"NameExtraClip": "Klip",
|
||||
"NameExtraDeletedScene": "Obrisana Scena",
|
||||
"NameExtraFeaturette": "Promotivni video",
|
||||
"NameExtraInterview": "Intervju",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Uzorak",
|
||||
"NameExtraScene": "Scena",
|
||||
"NameExtraShort": "Kratki film",
|
||||
"NameExtraThemeSong": "Glavna Pjesma",
|
||||
"NameExtraThemeVideo": "Tema videa",
|
||||
"NameExtraTrailer": "Trailer"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.",
|
||||
"CleanupUserDataTask": "Felhasználói adatok tisztítása feladat",
|
||||
"Original": "Eredeti",
|
||||
"LyricDownloadFailureFromForItem": "Dalszöveg letöltése {0}-tól {1}-hez sikertelen"
|
||||
"LyricDownloadFailureFromForItem": "Dalszöveg letöltése {0}-tól {1}-hez sikertelen",
|
||||
"NameExtraBehindTheScenes": "Színfalak mögött",
|
||||
"NameExtraClip": "Klip",
|
||||
"NameExtraDeletedScene": "Törölt jelenet",
|
||||
"NameExtraFeaturette": "Kísérő film",
|
||||
"NameExtraInterview": "Interjú",
|
||||
"NameExtraSample": "Minta",
|
||||
"NameExtraScene": "Jelenet",
|
||||
"NameExtraShort": "Rövidfilm",
|
||||
"NameExtraThemeSong": "Főcímdal",
|
||||
"NameExtraThemeVideo": "Főcímvideó",
|
||||
"NameExtraTrailer": "Előzetes",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameExtraNumbered": "{0} {1}"
|
||||
}
|
||||
|
||||
@@ -24,5 +24,10 @@
|
||||
"TaskDownloadMissingSubtitles": "Ներբեռնել պակասող ենթագրերը",
|
||||
"AppDeviceValues": "Հավելված` {0}, Սարք `{1}",
|
||||
"ChapterNameValue": "Գլուխ {0}",
|
||||
"Collections": "Հավաքածուներ"
|
||||
"Collections": "Հավաքածուներ",
|
||||
"Artists": "Երաժիշտներ",
|
||||
"Default": "Լռելյայն",
|
||||
"Favorites": "Ընտրյալ",
|
||||
"Forced": "Ստիպուած",
|
||||
"Genres": "Ոճ"
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"UserDownloadingItemWithValues": "{0} hleður niður {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
|
||||
"Shows": "Þættir",
|
||||
"TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
|
||||
"TaskRefreshChannelsDescription": "Endurhleður upplýsingum netrása.",
|
||||
"TaskRefreshChannels": "Endurhlaða Rásir",
|
||||
"TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
|
||||
"TaskCleanTranscode": "Hreinsa Umkóðunarmöppu",
|
||||
@@ -80,7 +80,7 @@
|
||||
"TasksMaintenanceCategory": "Viðhald",
|
||||
"Default": "Sjálfgefið",
|
||||
"TaskCleanActivityLog": "Hreinsa athafnaskrá",
|
||||
"TaskRefreshPeople": "Endurnýja fólk",
|
||||
"TaskRefreshPeople": "Endurnýja upplýsingar um fólk",
|
||||
"TaskDownloadMissingSubtitles": "Sækja texta sem vantar",
|
||||
"TaskOptimizeDatabase": "Fínstilla gagnagrunn",
|
||||
"Undefined": "Óskilgreint",
|
||||
@@ -95,8 +95,8 @@
|
||||
"TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.",
|
||||
"Forced": "Þvingað",
|
||||
"External": "Útvær",
|
||||
"TaskRefreshTrickplayImagesDescription": "Býr til hraðspilunarmyndir fyrir myndbönd í virkum söfnum.",
|
||||
"TaskRefreshTrickplayImages": "Búa til hraðspilunarmyndir",
|
||||
"TaskRefreshTrickplayImagesDescription": "Býr til hraðspilunarmyndir (Trickplay) fyrir myndbönd í virkum söfnum.",
|
||||
"TaskRefreshTrickplayImages": "Búa til hraðspilunarmyndir (Trickplay)",
|
||||
"TaskAudioNormalization": "Hljóðstöðlun",
|
||||
"TaskAudioNormalizationDescription": "Leitar að hljóðstöðlunargögnum í skrám.",
|
||||
"TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög",
|
||||
@@ -108,5 +108,18 @@
|
||||
"Original": "Upprunaleg",
|
||||
"TaskExtractMediaSegmentsDescription": "Sækir myndbúta úr viðbótum þar sem MediaSegment er virkt.",
|
||||
"TaskMoveTrickplayImages": "Flytja geymslustað fyrir Trickplay-myndir",
|
||||
"TaskMoveTrickplayImagesDescription": "Flytur fyrirliggjandi Trickplay-skrár í samræmi við stillingar safnsins."
|
||||
"TaskMoveTrickplayImagesDescription": "Flytur fyrirliggjandi Trickplay-skrár í samræmi við stillingar safnsins.",
|
||||
"NameExtraBehindTheScenes": "Bak við tjöldin",
|
||||
"NameExtraClip": "Brot",
|
||||
"NameExtraDeletedScene": "Eydd atriði",
|
||||
"NameExtraFeaturette": "Stutt heimildarmynd",
|
||||
"NameExtraInterview": "Viðtal",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Sýnishorn",
|
||||
"NameExtraScene": "Sena",
|
||||
"NameExtraShort": "Stuttmynd",
|
||||
"NameExtraThemeSong": "Þema lag",
|
||||
"NameExtraThemeVideo": "Þema myndband",
|
||||
"NameExtraTrailer": "Stikla",
|
||||
"NameExtraUnknown": "Aukaefni"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "Task di pulizia dei dati utente",
|
||||
"CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni.",
|
||||
"Original": "Originale",
|
||||
"LyricDownloadFailureFromForItem": "Scaricamento dei testi non riuscito da {0} per {1}"
|
||||
"LyricDownloadFailureFromForItem": "Scaricamento dei testi non riuscito da {0} per {1}",
|
||||
"NameExtraBehindTheScenes": "Dietro le scene",
|
||||
"NameExtraClip": "Filmato",
|
||||
"NameExtraDeletedScene": "Scena eliminata",
|
||||
"NameExtraInterview": "Intervista",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraScene": "Scena",
|
||||
"NameExtraSample": "Campione",
|
||||
"NameExtraShort": "Corto",
|
||||
"NameExtraThemeSong": "Sigla musicale",
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraFeaturette": "Caratteristica",
|
||||
"NameExtraThemeVideo": "Video tematico",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,8 @@
|
||||
"CleanupUserDataTask": "사용자 데이터 정리 작업",
|
||||
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다.",
|
||||
"LyricDownloadFailureFromForItem": "{1}에 대한 가사를 {0}에서 다운로드하지 못했습니다",
|
||||
"Original": "원본"
|
||||
"Original": "원본",
|
||||
"NameExtraClip": "클립",
|
||||
"NameExtraDeletedScene": "삭제된 장면",
|
||||
"NameExtraInterview": "인터뷰"
|
||||
}
|
||||
|
||||
@@ -104,5 +104,21 @@
|
||||
"TaskKeyframeExtractorDescription": "Extrahéiert Schlësselbiller aus Videodateien, fir méi präzis HLS-Playlisten ze erstellen. Dës Aufgab kann eng längere Zäit daueren.",
|
||||
"TaskRefreshChannelsDescription": "Aktualiséiert Informatiounen iwwer Internetkanäl.",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrahéiert oder kritt Mediesegmenter aus Plugins, déi MediaSegment ënnerstëtzen.",
|
||||
"TaskOptimizeDatabaseDescription": "Kompriméiert d’Datebank a schneit de fräie Speicherplatz zou. Dës Aufgab no engem Bibliothéik-Scan oder anere Ännerungen, déi Datebankmodifikatioune mat sech bréngen, auszeféieren, kann d’Performance verbesseren."
|
||||
"TaskOptimizeDatabaseDescription": "Kompriméiert d’Datebank a schneit de fräie Speicherplatz zou. Dës Aufgab no engem Bibliothéik-Scan oder anere Ännerungen, déi Datebankmodifikatioune mat sech bréngen, auszeféieren, kann d’Performance verbesseren.",
|
||||
"LyricDownloadFailureFromForItem": "Feeler beim Download vun de Songtexter vun {0} fir {1}",
|
||||
"Original": "Original",
|
||||
"CleanupUserDataTask": "Aufgab fir Berengege vu Benotzerdaten",
|
||||
"CleanupUserDataTaskDescription": "Läscht all Benotzerdaten (Ofspillstatus, Favoritestatus, asw.) vu Medien, déi zënter mindestens 90 Deeg net méi besteeënd sinn.",
|
||||
"NameExtraBehindTheScenes": "Hannert de Kulissen",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Geläschte Scène",
|
||||
"NameExtraFeaturette": "Featurette",
|
||||
"NameExtraInterview": "Interview",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Beispill",
|
||||
"NameExtraScene": "Scène",
|
||||
"NameExtraShort": "Kuerzfilm",
|
||||
"NameExtraThemeSong": "Theme-Lidd",
|
||||
"NameExtraThemeVideo": "Theme-Video",
|
||||
"NameExtraTrailer": "Bande-Annonce"
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
"Artists": "Atlikėjai",
|
||||
"AuthenticationSucceededWithUserName": "{0} sėkmingai autentifikuota",
|
||||
"Books": "Knygos",
|
||||
"ChapterNameValue": "Scena{0}",
|
||||
"Collections": "Rinkiniai",
|
||||
"ChapterNameValue": "Skyrius{0}",
|
||||
"Collections": "Kolekcijos",
|
||||
"FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti",
|
||||
"Favorites": "Mėgstami",
|
||||
"Folders": "Katalogai",
|
||||
"Folders": "Aplankai",
|
||||
"Genres": "Žanrai",
|
||||
"HeaderContinueWatching": "Žiūrėti toliau",
|
||||
"HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
|
||||
"HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
|
||||
"HeaderFavoriteEpisodes": "Mėgstami Epizodai",
|
||||
"HeaderFavoriteShows": "Mėgstamos TV Laidos",
|
||||
"HeaderLiveTV": "Tiesioginė TV",
|
||||
"HeaderNextUp": "Toliau",
|
||||
"HomeVideos": "Namų vaizdo įrašai",
|
||||
@@ -26,14 +26,14 @@
|
||||
"NameInstallFailed": "{0} diegimo klaida",
|
||||
"NameSeasonNumber": "Sezonas {0}",
|
||||
"NameSeasonUnknown": "Sezonas neatpažintas",
|
||||
"NewVersionIsAvailable": "Nauja \"Jellyfin Server\" versija yra prieinama atsisiuntimui.",
|
||||
"NewVersionIsAvailable": "Nauja Jellyfin Server versija yra prieinama atsisiuntimui.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Galimi programos atnaujinimai",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Programos atnaujinimai įdiegti",
|
||||
"NotificationOptionAudioPlayback": "Garso atkūrimas pradėtas",
|
||||
"NotificationOptionAudioPlaybackStopped": "Garso atkūrimas sustabdytas",
|
||||
"NotificationOptionCameraImageUploaded": "Kameros vaizdai įkelti",
|
||||
"NotificationOptionCameraImageUploaded": "Kameros atvaizdai įkelti",
|
||||
"NotificationOptionInstallationFailed": "Diegimo klaida",
|
||||
"NotificationOptionNewLibraryContent": "Naujas turinys įkeltas",
|
||||
"NotificationOptionNewLibraryContent": "Pridėtas naujas turinys",
|
||||
"NotificationOptionPluginError": "Įskiepio klaida",
|
||||
"NotificationOptionPluginInstalled": "Įskiepis įdiegtas",
|
||||
"NotificationOptionPluginUninstalled": "Įskiepis išdiegtas",
|
||||
@@ -51,7 +51,7 @@
|
||||
"Shows": "Laidos",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server kraunasi. Netrukus pabandykite dar kartą.",
|
||||
"SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}",
|
||||
"TvShows": "TV laidos",
|
||||
"TvShows": "TV Laidos",
|
||||
"UserCreatedWithName": "Buvo sukurtas {0} naudotojas",
|
||||
"UserDeletedWithName": "Naudotojas {0} ištrintas",
|
||||
"UserDownloadingItemWithValues": "{0} siunčiasi {1}",
|
||||
@@ -59,14 +59,14 @@
|
||||
"UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}",
|
||||
"UserOnlineFromDevice": "{0} prisijungęs iš {1}",
|
||||
"UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} atkuriama {1} į {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} baigė atkūrimą {1} į {2}",
|
||||
"VersionNumber": "Versija {0}",
|
||||
"TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.",
|
||||
"TaskUpdatePlugins": "Atnaujinti įskieius",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.",
|
||||
"TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
|
||||
"TaskCleanTranscode": "Išvalyti perkodavimo katalogą",
|
||||
"TaskCleanTranscodeDescription": "Ištrina dienos senumo transkodavimo failus.",
|
||||
"TaskCleanTranscode": "Išvalyti transkodavimo katalogą",
|
||||
"TaskRefreshLibraryDescription": "Skenuoja medijos biblioteką, ieškodamas naujų failų, ir atnaujina metaduomenis.",
|
||||
"TaskRefreshLibrary": "Skenuoti medijos biblioteką",
|
||||
"TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
|
||||
@@ -76,8 +76,8 @@
|
||||
"TaskRefreshPeople": "Atnaujinti žmones",
|
||||
"TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
|
||||
"TaskCleanLogs": "Išvalyti žurnalą",
|
||||
"TaskRefreshChapterImagesDescription": "Sukuria vaizdo įrašų, kuriuose yra skyrių, miniatiūras.",
|
||||
"TaskRefreshChapterImages": "Ištraukti skyrių vaizdus",
|
||||
"TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašams, kuriuose yra skyriai.",
|
||||
"TaskRefreshChapterImages": "Ištraukti skyrių atvaizdus",
|
||||
"TaskCleanCache": "Išvalyti talpyklą",
|
||||
"TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.",
|
||||
"TasksChannelsCategory": "Internetiniai kanalai",
|
||||
@@ -87,7 +87,7 @@
|
||||
"TaskCleanActivityLog": "Išvalyti veiklos žurnalą",
|
||||
"Undefined": "Neapibrėžtas",
|
||||
"Forced": "Priverstinis",
|
||||
"Default": "Numatytas",
|
||||
"Default": "Numatytasis",
|
||||
"TaskCleanActivityLogDescription": "Ištrina senesnius nei nustatytas amžius veiklos žurnalo įrašus.",
|
||||
"TaskOptimizeDatabase": "Optimizuoti duomenų bazę",
|
||||
"TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.",
|
||||
@@ -96,15 +96,30 @@
|
||||
"External": "Išorinis",
|
||||
"HearingImpaired": "Su klausos sutrikimais",
|
||||
"TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
|
||||
"TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
|
||||
"TaskRefreshTrickplayImagesDescription": "Sukuria vaizdo įrašų, esančių įgalintose bibliotekose, Trickplay peržiūras.",
|
||||
"TaskAudioNormalization": "Garso normalizavimas",
|
||||
"TaskAudioNormalizationDescription": "Skenuoja failus, ieškant garso normalizavimo duomenų.",
|
||||
"TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
|
||||
"TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus",
|
||||
"TaskDownloadMissingLyrics": "Atsisiųsti trūkstamus dainų tekstus",
|
||||
"TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų įskiepių.",
|
||||
"TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą",
|
||||
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
|
||||
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius",
|
||||
"TaskMoveTrickplayImages": "Pakeisti Trickplay atvaizdų vietą",
|
||||
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius Trickplay failus pagal bibliotekos nustatymus.",
|
||||
"TaskDownloadMissingLyricsDescription": "Atsisiųsti dainų tekstus",
|
||||
"CleanupUserDataTask": "Naudotojo duomenų valymo užduotis",
|
||||
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.)."
|
||||
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamą būseną ir t. t.).",
|
||||
"LyricDownloadFailureFromForItem": "Nepavyko atsisiųsti dainos teksto iš {0}, skirto {1}",
|
||||
"NameExtraBehindTheScenes": "Užkulisiuose",
|
||||
"NameExtraClip": "Klipas",
|
||||
"NameExtraDeletedScene": "Ištrinta scena",
|
||||
"NameExtraInterview": "Interviu",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Pavyzdys",
|
||||
"NameExtraScene": "Scena",
|
||||
"NameExtraThemeSong": "Teminė daina",
|
||||
"NameExtraThemeVideo": "Teminis vaizdo įrašas",
|
||||
"NameExtraTrailer": "Anonsas",
|
||||
"NameExtraUnknown": "Papildomas",
|
||||
"Original": "Originalus",
|
||||
"NameExtraFeaturette": "Trumpametražis filmas",
|
||||
"NameExtraShort": "Trumpas filmukas"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,17 @@
|
||||
"CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums",
|
||||
"CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas.",
|
||||
"Original": "Oriģināls",
|
||||
"LyricDownloadFailureFromForItem": "Dziesmu vārdi nevarēja tikt lejupielādēti no {0} priekš {1}"
|
||||
"LyricDownloadFailureFromForItem": "Dziesmu vārdi nevarēja tikt lejupielādēti no {0} priekš {1}",
|
||||
"NameExtraBehindTheScenes": "Aiz kadra",
|
||||
"NameExtraClip": "Klips",
|
||||
"NameExtraDeletedScene": "Izdzēsta aina",
|
||||
"NameExtraFeaturette": "Īsfilma",
|
||||
"NameExtraInterview": "Intervija",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Paraugs",
|
||||
"NameExtraScene": "Aina",
|
||||
"NameExtraShort": "Īsfilma",
|
||||
"NameExtraThemeSong": "Motīvu dziesma",
|
||||
"NameExtraThemeVideo": "Tēmas video",
|
||||
"NameExtraTrailer": "Treileris"
|
||||
}
|
||||
|
||||
@@ -106,5 +106,11 @@
|
||||
"TaskAudioNormalization": "Normalisasi Audio",
|
||||
"TaskAudioNormalizationDescription": "Mengimbas fail-fail untuk data normalisasi audio.",
|
||||
"CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (keadaan tontonan, status kegemaran, dan sebagainya) daripada media yang tidak lagi wujud sekurang-kurangnya selama 90 hari.",
|
||||
"CleanupUserDataTask": "Tugas pembersihan data pengguna"
|
||||
"CleanupUserDataTask": "Tugas pembersihan data pengguna",
|
||||
"LyricDownloadFailureFromForItem": "Lirik gagal dimuat turun dari {0} untuk {1}",
|
||||
"NameExtraBehindTheScenes": "Di Sebalik Takbir",
|
||||
"NameExtraClip": "Klip",
|
||||
"NameExtraInterview": "Temu bual",
|
||||
"NameExtraThemeSong": "Lagu Tema",
|
||||
"NameExtraThemeVideo": "Video Tema"
|
||||
}
|
||||
|
||||
@@ -106,5 +106,15 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til biblioteksinstillingene.",
|
||||
"TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment.",
|
||||
"CleanupUserDataTaskDescription": "Sletter all brukerdata (avspillings-status, favoritter osv.) fra innhold som har vært utilgjengelig i minst 90 dager.",
|
||||
"CleanupUserDataTask": "Oppgave for opprydding av brukerdata"
|
||||
"CleanupUserDataTask": "Oppgave for opprydding av brukerdata",
|
||||
"NameExtraBehindTheScenes": "Bak kulissene",
|
||||
"NameExtraDeletedScene": "Slettet scene",
|
||||
"NameExtraFeaturette": "Presentasjonsfilm",
|
||||
"NameExtraInterview": "Intervju",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Prøve",
|
||||
"NameExtraScene": "Scene",
|
||||
"NameExtraThemeSong": "Tema-låt",
|
||||
"NameExtraThemeVideo": "Tema-video",
|
||||
"NameExtraTrailer": "Trailer"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "Opruimtaak gebruikersdata",
|
||||
"Genres": "Genres",
|
||||
"Original": "Oorspronkelijk",
|
||||
"LyricDownloadFailureFromForItem": "Downloaden van liedteksten voor {1} van {0} mislukt"
|
||||
"LyricDownloadFailureFromForItem": "Downloaden van liedteksten voor {1} van {0} mislukt",
|
||||
"NameExtraBehindTheScenes": "Achter de schermen",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Geschrapte scène",
|
||||
"NameExtraFeaturette": "Featurette",
|
||||
"NameExtraInterview": "Vraaggesprek",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Voorbeeldfragment",
|
||||
"NameExtraScene": "Scène",
|
||||
"NameExtraShort": "Korte film",
|
||||
"NameExtraThemeSong": "Themamuziek",
|
||||
"NameExtraThemeVideo": "Themavideo",
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraUnknown": "Extra inhoud"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
{
|
||||
"AppDeviceValues": "Aplicacion: {0}, Periferic: {1}"
|
||||
"AppDeviceValues": "Aplicacion: {0}, Periferic: {1}",
|
||||
"Books": "Libres",
|
||||
"Artists": "Artistas",
|
||||
"Collections": "Collecciones",
|
||||
"ChapterNameValue": "Capitol {0}",
|
||||
"External": "Extèrn",
|
||||
"Folders": "Dorsièrs",
|
||||
"Favorites": "Favorits",
|
||||
"HeaderContinueWatching": "Contunhar de regardar",
|
||||
"HeaderFavoriteEpisodes": "Episòdis Favorits",
|
||||
"AuthenticationSucceededWithUserName": "{0} autentificat amb succès",
|
||||
"HeaderFavoriteShows": "Serias Favoritas",
|
||||
"HeaderLiveTV": "TV en dirècte",
|
||||
"HeaderNextUp": "Seguent",
|
||||
"HearingImpaired": "Amb de deficiéncias auditivas",
|
||||
"Movies": "Filmes",
|
||||
"Music": "Musica",
|
||||
"Latest": "Darrièr",
|
||||
"Forced": "Forçat",
|
||||
"Default": "Defaut",
|
||||
"Genres": "Genres",
|
||||
"HomeVideos": "Vidèos d'Acuèlh",
|
||||
"Inherit": "Eiretar",
|
||||
"LabelIpAddressValue": "Adreça IP: {0}",
|
||||
"LabelRunningTimeValue": "Temps d'execucion : {0}"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Usuwa wszystkie dane użytkownika (stan oglądanych, status ulubionych itp.) z mediów, które nie są dostępne od co najmniej 90 dni.",
|
||||
"CleanupUserDataTask": "Zadanie czyszczenia danych użytkownika",
|
||||
"Original": "Oryginalny",
|
||||
"LyricDownloadFailureFromForItem": "Błąd podczas pobierania tekstu piosenki z {0} dla {1}"
|
||||
"LyricDownloadFailureFromForItem": "Błąd podczas pobierania tekstu piosenki z {0} dla {1}",
|
||||
"NameExtraBehindTheScenes": "Za kulisami",
|
||||
"NameExtraClip": "Urywek",
|
||||
"NameExtraDeletedScene": "Usunięta scena",
|
||||
"NameExtraFeaturette": "Film średniometrażowy",
|
||||
"NameExtraInterview": "Wywiad",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Fragment",
|
||||
"NameExtraScene": "Scena",
|
||||
"NameExtraShort": "Film krótkometrażowy",
|
||||
"NameExtraThemeSong": "Czołówka",
|
||||
"NameExtraThemeVideo": "Wideo wprowadzające",
|
||||
"NameExtraTrailer": "Zwiastun",
|
||||
"NameExtraUnknown": "Dodatek"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de dados do usuário",
|
||||
"CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias.",
|
||||
"LyricDownloadFailureFromForItem": "Download das Letras falharam em {0} para o item {1}",
|
||||
"Original": "Original"
|
||||
"Original": "Original",
|
||||
"NameExtraBehindTheScenes": "Nos Bastidores",
|
||||
"NameExtraClip": "Clipe",
|
||||
"NameExtraDeletedScene": "cena Extra",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Trecho",
|
||||
"NameExtraScene": "Cena",
|
||||
"NameExtraShort": "Curta-metragem",
|
||||
"NameExtraThemeSong": "Música Tema",
|
||||
"NameExtraThemeVideo": "Vídeo de Abertura",
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameExtraFeaturette": "Nos Bastidores",
|
||||
"NameExtraInterview": "Entrevista"
|
||||
}
|
||||
|
||||
@@ -5,108 +5,121 @@
|
||||
"Books": "Livros",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"Collections": "Coleções",
|
||||
"FailedLoginAttemptWithUserName": "Tentativa de login falhada a partir de {0}",
|
||||
"FailedLoginAttemptWithUserName": "Tentativa falhada de início de sessão do utilizador {0}",
|
||||
"Favorites": "Favoritos",
|
||||
"Folders": "Pastas",
|
||||
"Genres": "Géneros",
|
||||
"HeaderContinueWatching": "Continuar a ver",
|
||||
"HeaderFavoriteEpisodes": "Episódios Favoritos",
|
||||
"HeaderFavoriteShows": "Séries Favoritas",
|
||||
"HeaderLiveTV": "TV em Direto",
|
||||
"HeaderNextUp": "A Seguir",
|
||||
"HomeVideos": "Vídeos Caseiros",
|
||||
"HeaderFavoriteEpisodes": "Episódios favoritos",
|
||||
"HeaderFavoriteShows": "Séries favoritas",
|
||||
"HeaderLiveTV": "TV em direto",
|
||||
"HeaderNextUp": "A seguir",
|
||||
"HomeVideos": "Vídeos caseiros",
|
||||
"Inherit": "Herdar",
|
||||
"LabelIpAddressValue": "Endereço IP: {0}",
|
||||
"LabelRunningTimeValue": "Duração: {0}",
|
||||
"Latest": "Mais Recente",
|
||||
"MixedContent": "Conteúdo Misto",
|
||||
"Latest": "Mais recente",
|
||||
"MixedContent": "Conteúdo misto",
|
||||
"Movies": "Filmes",
|
||||
"Music": "Música",
|
||||
"MusicVideos": "Videoclipes",
|
||||
"NameInstallFailed": "{0} falha na instalação",
|
||||
"NameInstallFailed": "Falha na instalação de {0}",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameSeasonUnknown": "Temporada Desconhecida",
|
||||
"NameSeasonUnknown": "Temporada desconhecida",
|
||||
"NewVersionIsAvailable": "Está disponível para transferência uma nova versão do servidor Jellyfin.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Atualização de aplicação disponível",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Atualização de aplicação instalada",
|
||||
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
|
||||
"NotificationOptionAudioPlayback": "Reprodução de áudio iniciada",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodução de áudio interrompida",
|
||||
"NotificationOptionCameraImageUploaded": "Imagem da câmara enviada",
|
||||
"NotificationOptionInstallationFailed": "Falha na instalação",
|
||||
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
|
||||
"NotificationOptionPluginError": "Falha na extensão",
|
||||
"NotificationOptionPluginInstalled": "Extensão instalada",
|
||||
"NotificationOptionPluginUninstalled": "Extensão desinstalada",
|
||||
"NotificationOptionPluginUpdateInstalled": "Extensão atualizada",
|
||||
"NotificationOptionPluginError": "Falha no plugin",
|
||||
"NotificationOptionPluginInstalled": "Plugin instalado",
|
||||
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
|
||||
"NotificationOptionPluginUpdateInstalled": "Atualização de plugin instalada",
|
||||
"NotificationOptionServerRestartRequired": "Necessário reiniciar o servidor",
|
||||
"NotificationOptionTaskFailed": "Falha em tarefa agendada",
|
||||
"NotificationOptionUserLockedOut": "Utilizador bloqueado",
|
||||
"NotificationOptionVideoPlayback": "Reprodução do vídeo iniciada",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodução do vídeo parada",
|
||||
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo interrompida",
|
||||
"Photos": "Fotografias",
|
||||
"PluginInstalledWithName": "{0} foi instalado",
|
||||
"PluginUninstalledWithName": "{0} foi desinstalado",
|
||||
"PluginUpdatedWithName": "{0} foi atualizado",
|
||||
"ScheduledTaskFailedWithName": "{0} falhou",
|
||||
"Shows": "Séries",
|
||||
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente mais tarde.",
|
||||
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas a partir de {0} para {1}",
|
||||
"TvShows": "Séries",
|
||||
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tenta novamente dentro de instantes.",
|
||||
"SubtitleDownloadFailureFromForItem": "Falha ao transferir legendas de {0} para {1}",
|
||||
"TvShows": "Séries de TV",
|
||||
"UserCreatedWithName": "Utilizador {0} criado",
|
||||
"UserDeletedWithName": "Utilizador {0} apagado",
|
||||
"UserDeletedWithName": "Utilizador {0} eliminado",
|
||||
"UserDownloadingItemWithValues": "{0} está a transferir {1}",
|
||||
"UserLockedOutWithName": "Utilizador {0} bloqueado",
|
||||
"UserOfflineFromDevice": "{0} desligou-se a partir de {1}",
|
||||
"UserOnlineFromDevice": "{0} ligou-se a partir de {1}",
|
||||
"UserOfflineFromDevice": "{0} desligou-se de {1}",
|
||||
"UserOnlineFromDevice": "{0} está online em {1}",
|
||||
"UserPasswordChangedWithName": "Palavra-passe alterada para o utilizador {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
|
||||
"VersionNumber": "Versão {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas em falta baseado na configuração de metadados.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Procura na Internet legendas em falta com base na configuração dos metadados.",
|
||||
"TaskDownloadMissingSubtitles": "Transferir legendas em falta",
|
||||
"TaskRefreshChannelsDescription": "Atualizar informação sobre canais da Internet.",
|
||||
"TaskRefreshChannels": "Atualizar Canais",
|
||||
"TaskCleanTranscodeDescription": "Apagar ficheiros de transcode com mais de um dia.",
|
||||
"TaskCleanTranscode": "Limpar a Diretoria de Transcode",
|
||||
"TaskUpdatePluginsDescription": "Faz o download e instala updates para os plugins que estão configurados para atualizar automaticamente.",
|
||||
"TaskUpdatePlugins": "Atualizar Plugins",
|
||||
"TaskRefreshPeopleDescription": "Atualizar metadados para elenco e equipa técnica da tua mediateca.",
|
||||
"TaskRefreshPeople": "Atualizar Pessoas",
|
||||
"TaskCleanLogsDescription": "Apagar ficheiros de log que têm mais de {0} dias.",
|
||||
"TaskCleanLogs": "Limpar a Diretoria de Logs",
|
||||
"TaskRefreshLibraryDescription": "Analisar a mediateca para novos ficheiros e atualizar os metadados.",
|
||||
"TaskRefreshLibrary": "Analisar mediateca",
|
||||
"TaskRefreshChapterImagesDescription": "Criar thumbnails para os vídeos que têm capítulos.",
|
||||
"TaskRefreshChapterImages": "Extrair Imagens dos Capítulos",
|
||||
"TaskCleanCacheDescription": "Apagar ficheiros em cache que já não são necessários.",
|
||||
"TaskCleanCache": "Limpar Cache",
|
||||
"TaskRefreshChannelsDescription": "Atualiza as informações dos canais da Internet.",
|
||||
"TaskRefreshChannels": "Atualizar canais",
|
||||
"TaskCleanTranscodeDescription": "Elimina ficheiros de transcodificação com mais de um dia.",
|
||||
"TaskCleanTranscode": "Limpar pasta de transcodificação",
|
||||
"TaskUpdatePluginsDescription": "Transfere e instala atualizações dos plugins configurados para atualização automática.",
|
||||
"TaskUpdatePlugins": "Atualizar plugins",
|
||||
"TaskRefreshPeopleDescription": "Atualiza os metadados de atores e realizadores na tua biblioteca multimédia.",
|
||||
"TaskRefreshPeople": "Atualizar pessoas",
|
||||
"TaskCleanLogsDescription": "Elimina ficheiros de registo com mais de {0} dias.",
|
||||
"TaskCleanLogs": "Limpar pasta de registos",
|
||||
"TaskRefreshLibraryDescription": "Analisa a biblioteca multimédia à procura de novos ficheiros e atualiza os metadados.",
|
||||
"TaskRefreshLibrary": "Analisar biblioteca multimédia",
|
||||
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
|
||||
"TaskRefreshChapterImages": "Extrair imagens dos capítulos",
|
||||
"TaskCleanCacheDescription": "Elimina ficheiros de cache que já não são necessários ao sistema.",
|
||||
"TaskCleanCache": "Limpar pasta de cache",
|
||||
"TasksChannelsCategory": "Canais da Internet",
|
||||
"TasksApplicationCategory": "Aplicação",
|
||||
"TasksLibraryCategory": "Mediateca",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksMaintenanceCategory": "Manutenção",
|
||||
"TaskCleanActivityLogDescription": "Apaga as entradas do registo de atividade anteriores à data configurada.",
|
||||
"TaskCleanActivityLogDescription": "Elimina as entradas do registo de atividade mais antigas do que o período configurado.",
|
||||
"TaskCleanActivityLog": "Limpar registo de atividade",
|
||||
"Undefined": "Indefinido",
|
||||
"Forced": "Forçado",
|
||||
"Default": "Predefinição",
|
||||
"TaskOptimizeDatabaseDescription": "Otimiza e liberta espaço livre na base de dados. A execução desta tarefa depois de analisar a mediateca ou efetuar outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta a base de dados e liberta espaço não utilizado. A execução desta tarefa depois de analisar a biblioteca ou de outras alterações que modifiquem a base de dados pode melhorar o desempenho.",
|
||||
"TaskOptimizeDatabase": "Otimizar base de dados",
|
||||
"TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.",
|
||||
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
|
||||
"TaskKeyframeExtractorDescription": "Extrai fotogramas-chave de ficheiros de vídeo para criar playlists HLS mais precisas. Esta tarefa pode demorar algum tempo.",
|
||||
"TaskKeyframeExtractor": "Extrator de fotogramas-chave",
|
||||
"External": "Externo",
|
||||
"HearingImpaired": "Surdo",
|
||||
"HearingImpaired": "Deficiência auditiva",
|
||||
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
|
||||
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
|
||||
"TaskAudioNormalization": "Normalização de áudio",
|
||||
"TaskExtractMediaSegments": "Analisar segmentos de multimédia",
|
||||
"TaskDownloadMissingLyrics": "Transferir letra em falta",
|
||||
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
|
||||
"TaskDownloadMissingLyricsDescription": "Transferir letra para músicas",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
|
||||
"CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.",
|
||||
"TaskExtractMediaSegments": "Analisar segmentos multimédia",
|
||||
"TaskDownloadMissingLyrics": "Transferir letras em falta",
|
||||
"TaskMoveTrickplayImages": "Migrar localização das imagens de trickplay",
|
||||
"TaskDownloadMissingLyricsDescription": "Transfere letras de músicas",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos multimédia de plugins com MediaSegment ativado.",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de trickplay existentes de acordo com as definições da biblioteca.",
|
||||
"CleanupUserDataTaskDescription": "Remove todos os dados de utilizador (estado de reprodução, estado de favorito, etc.) de conteúdos multimédia que já não estejam presentes há pelo menos 90 dias.",
|
||||
"CleanupUserDataTask": "Limpeza de dados de utilizador",
|
||||
"Original": "Original",
|
||||
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}"
|
||||
"LyricDownloadFailureFromForItem": "Falha ao transferir letras de {0} para {1}",
|
||||
"NameExtraDeletedScene": "Cena eliminada",
|
||||
"NameExtraInterview": "Entrevista",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameExtraBehindTheScenes": "Bastidores",
|
||||
"NameExtraSample": "Amostra",
|
||||
"NameExtraScene": "Cena",
|
||||
"NameExtraClip": "Excerto",
|
||||
"NameExtraFeaturette": "Minidocumentário",
|
||||
"NameExtraThemeSong": "Tema musical",
|
||||
"NameExtraThemeVideo": "Vídeo temático",
|
||||
"NameExtraShort": "Curta-metragem",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraTrailer": "Trailer"
|
||||
}
|
||||
|
||||
@@ -1,112 +1,125 @@
|
||||
{
|
||||
"HeaderLiveTV": "TV Em Direto",
|
||||
"HeaderLiveTV": "TV em direto",
|
||||
"Collections": "Coleções",
|
||||
"Books": "Livros",
|
||||
"Artists": "Artistas",
|
||||
"HeaderNextUp": "A Seguir",
|
||||
"HeaderFavoriteEpisodes": "Episódios Favoritos",
|
||||
"HeaderFavoriteShows": "Séries Favoritas",
|
||||
"HeaderNextUp": "A seguir",
|
||||
"HeaderFavoriteEpisodes": "Episódios favoritos",
|
||||
"HeaderFavoriteShows": "Séries favoritas",
|
||||
"HeaderContinueWatching": "Continuar a ver",
|
||||
"Genres": "Géneros",
|
||||
"Folders": "Pastas",
|
||||
"Favorites": "Favoritos",
|
||||
"UserDownloadingItemWithValues": "{0} está sendo baixado {1}",
|
||||
"UserDownloadingItemWithValues": "{0} está a transferir {1}",
|
||||
"VersionNumber": "Versão {0}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} está reproduzindo {1} em {2}",
|
||||
"UserPasswordChangedWithName": "A senha do usuário {0} foi alterada",
|
||||
"UserOnlineFromDevice": "{0} está online a partir de {1}",
|
||||
"UserOfflineFromDevice": "{0} desconectou-se a partir de {1}",
|
||||
"UserLockedOutWithName": "O usuário {0} foi bloqueado",
|
||||
"UserDeletedWithName": "O usuário {0} foi removido",
|
||||
"UserCreatedWithName": "O usuário {0} foi criado",
|
||||
"TvShows": "Séries",
|
||||
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas de {0} para {1}",
|
||||
"StartupEmbyServerIsLoading": "O servidor Jellyfin está iniciando. Tente novamente dentro de momentos.",
|
||||
"UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
|
||||
"UserPasswordChangedWithName": "Palavra-passe alterada para o utilizador {0}",
|
||||
"UserOnlineFromDevice": "{0} está online em {1}",
|
||||
"UserOfflineFromDevice": "{0} desligou-se de {1}",
|
||||
"UserLockedOutWithName": "Utilizador {0} bloqueado",
|
||||
"UserDeletedWithName": "Utilizador {0} eliminado",
|
||||
"UserCreatedWithName": "Utilizador {0} criado",
|
||||
"TvShows": "Séries de TV",
|
||||
"SubtitleDownloadFailureFromForItem": "Falha ao transferir legendas de {0} para {1}",
|
||||
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tenta novamente dentro de instantes.",
|
||||
"ScheduledTaskFailedWithName": "{0} falhou",
|
||||
"PluginUpdatedWithName": "{0} foi atualizado",
|
||||
"PluginUninstalledWithName": "{0} foi desinstalado",
|
||||
"PluginInstalledWithName": "{0} foi instalado",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo parada",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo interrompida",
|
||||
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
|
||||
"NotificationOptionUserLockedOut": "Usuário bloqueado",
|
||||
"NotificationOptionTaskFailed": "Falha na tarefa agendada",
|
||||
"NotificationOptionServerRestartRequired": "É necessário reiniciar o servidor",
|
||||
"NotificationOptionPluginUpdateInstalled": "Plugin atualizado",
|
||||
"NotificationOptionUserLockedOut": "Utilizador bloqueado",
|
||||
"NotificationOptionTaskFailed": "Falha em tarefa agendada",
|
||||
"NotificationOptionServerRestartRequired": "Necessário reiniciar o servidor",
|
||||
"NotificationOptionPluginUpdateInstalled": "Atualização de plugin instalada",
|
||||
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
|
||||
"NotificationOptionPluginInstalled": "Plugin instalado",
|
||||
"NotificationOptionPluginError": "Falha no plugin",
|
||||
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
|
||||
"NotificationOptionInstallationFailed": "Falha de instalação",
|
||||
"NotificationOptionCameraImageUploaded": "Imagem de câmera enviada",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
|
||||
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
|
||||
"NotificationOptionApplicationUpdateInstalled": "A atualização do aplicativo foi instalada",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Uma atualização do aplicativo está disponível",
|
||||
"NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para download.",
|
||||
"NameSeasonUnknown": "Temporada Desconhecida",
|
||||
"NotificationOptionInstallationFailed": "Falha na instalação",
|
||||
"NotificationOptionCameraImageUploaded": "Imagem da câmara enviada",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodução de áudio interrompida",
|
||||
"NotificationOptionAudioPlayback": "Reprodução de áudio iniciada",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Atualização de aplicação instalada",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Atualização de aplicação disponível",
|
||||
"NewVersionIsAvailable": "Está disponível para transferência uma nova versão do servidor Jellyfin.",
|
||||
"NameSeasonUnknown": "Temporada desconhecida",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameInstallFailed": "Falha na instalação de {0}",
|
||||
"MusicVideos": "Videoclipes",
|
||||
"Music": "Música",
|
||||
"MixedContent": "Conteúdo diverso",
|
||||
"Latest": "Mais Recente",
|
||||
"MixedContent": "Conteúdo misto",
|
||||
"Latest": "Mais recente",
|
||||
"LabelRunningTimeValue": "Duração: {0}",
|
||||
"LabelIpAddressValue": "Endereço de IP: {0}",
|
||||
"LabelIpAddressValue": "Endereço IP: {0}",
|
||||
"Inherit": "Herdar",
|
||||
"HomeVideos": "Vídeos Caseiros",
|
||||
"HomeVideos": "Vídeos caseiros",
|
||||
"Shows": "Séries",
|
||||
"Photos": "Fotografias",
|
||||
"Movies": "Filmes",
|
||||
"FailedLoginAttemptWithUserName": "Tentativa de início de sessão falhada a partir de {0}",
|
||||
"FailedLoginAttemptWithUserName": "Tentativa falhada de início de sessão do utilizador {0}",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
|
||||
"AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
|
||||
"TaskCleanCache": "Limpar Diretório de Cache",
|
||||
"TaskCleanCache": "Limpar pasta de cache",
|
||||
"TasksApplicationCategory": "Aplicação",
|
||||
"TasksLibraryCategory": "Mediateca",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksMaintenanceCategory": "Manutenção",
|
||||
"TaskRefreshChannels": "Atualizar Canais",
|
||||
"TaskUpdatePlugins": "Atualizar Plugins",
|
||||
"TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
|
||||
"TaskCleanLogs": "Limpar diretório de logs",
|
||||
"TaskRefreshLibrary": "Analisar mediateca",
|
||||
"TaskRefreshChannels": "Atualizar canais",
|
||||
"TaskUpdatePlugins": "Atualizar plugins",
|
||||
"TaskCleanLogsDescription": "Elimina ficheiros de registo com mais de {0} dias.",
|
||||
"TaskCleanLogs": "Limpar pasta de registos",
|
||||
"TaskRefreshLibrary": "Analisar biblioteca multimédia",
|
||||
"TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
|
||||
"TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
|
||||
"TasksChannelsCategory": "Canais de Internet",
|
||||
"TaskRefreshChapterImages": "Extrair Imagens do Capítulo",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Pesquisa na Internet as legendas em falta com base na configuração de metadados.",
|
||||
"TaskCleanCacheDescription": "Elimina ficheiros de cache que já não são necessários ao sistema.",
|
||||
"TasksChannelsCategory": "Canais da Internet",
|
||||
"TaskRefreshChapterImages": "Extrair imagens dos capítulos",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Procura na Internet legendas em falta com base na configuração dos metadados.",
|
||||
"TaskDownloadMissingSubtitles": "Transferir legendas em falta",
|
||||
"TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
|
||||
"TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
|
||||
"TaskCleanTranscode": "Limpar o diretório de Transcode",
|
||||
"TaskUpdatePluginsDescription": "Baixa e instala as atualizações para plug-ins configurados para atualização automática.",
|
||||
"TaskRefreshPeopleDescription": "Atualizar metadados para elenco e equipa técnica da tua mediateca.",
|
||||
"TaskRefreshChannelsDescription": "Atualiza as informações dos canais da Internet.",
|
||||
"TaskCleanTranscodeDescription": "Elimina ficheiros de transcodificação com mais de um dia.",
|
||||
"TaskCleanTranscode": "Limpar pasta de transcodificação",
|
||||
"TaskUpdatePluginsDescription": "Transfere e instala atualizações dos plugins configurados para atualização automática.",
|
||||
"TaskRefreshPeopleDescription": "Atualiza os metadados de atores e realizadores na tua biblioteca multimédia.",
|
||||
"TaskRefreshPeople": "Atualizar pessoas",
|
||||
"TaskRefreshLibraryDescription": "Analisar a mediateca para novos ficheiros e atualizar os metadados.",
|
||||
"TaskCleanActivityLog": "Limpar registro de atividade",
|
||||
"TaskRefreshLibraryDescription": "Analisa a biblioteca multimédia à procura de novos ficheiros e atualiza os metadados.",
|
||||
"TaskCleanActivityLog": "Limpar registo de atividade",
|
||||
"Undefined": "Indefinido",
|
||||
"Forced": "Forçado",
|
||||
"Default": "Predefinição",
|
||||
"TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado.",
|
||||
"TaskCleanActivityLogDescription": "Elimina as entradas do registo de atividade mais antigas do que o período configurado.",
|
||||
"TaskOptimizeDatabase": "Otimizar base de dados",
|
||||
"TaskOptimizeDatabaseDescription": "Otimiza e liberta espaço livre na base de dados. A execução desta tarefa depois de analisar a mediateca ou efetuar outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta a base de dados e liberta espaço não utilizado. A execução desta tarefa depois de analisar a biblioteca ou de outras alterações que modifiquem a base de dados pode melhorar o desempenho.",
|
||||
"External": "Externo",
|
||||
"HearingImpaired": "Problemas auditivos",
|
||||
"TaskKeyframeExtractor": "Extrator de quadro-chave",
|
||||
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
|
||||
"HearingImpaired": "Deficiência auditiva",
|
||||
"TaskKeyframeExtractor": "Extrator de fotogramas-chave",
|
||||
"TaskKeyframeExtractorDescription": "Extrai fotogramas-chave de ficheiros de vídeo para criar playlists HLS mais precisas. Esta tarefa pode demorar algum tempo.",
|
||||
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
|
||||
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
|
||||
"TaskAudioNormalization": "Normalização de áudio",
|
||||
"TaskDownloadMissingLyrics": "Transferir letra em falta",
|
||||
"TaskDownloadMissingLyricsDescription": "Transferir letra para músicas",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
|
||||
"TaskExtractMediaSegments": "Analisar segmentos de multimédia",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
|
||||
"CleanupUserDataTask": "Task de limpeza de dados do usuário",
|
||||
"CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de mídias que não estão presentes há pelo menos 90 dias.",
|
||||
"TaskDownloadMissingLyrics": "Transferir letras em falta",
|
||||
"TaskDownloadMissingLyricsDescription": "Transfere letras de músicas",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de trickplay existentes de acordo com as definições da biblioteca.",
|
||||
"TaskExtractMediaSegments": "Analisar segmentos multimédia",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos multimédia de plugins com MediaSegment ativado.",
|
||||
"TaskMoveTrickplayImages": "Migrar localização das imagens de trickplay",
|
||||
"CleanupUserDataTask": "Limpeza de dados de utilizador",
|
||||
"CleanupUserDataTaskDescription": "Remove todos os dados de utilizador (estado de reprodução, estado de favorito, etc.) de conteúdos multimédia que já não estejam presentes há pelo menos 90 dias.",
|
||||
"Original": "Original",
|
||||
"LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}"
|
||||
"LyricDownloadFailureFromForItem": "Falha ao transferir letras de {0} para {1}",
|
||||
"NameExtraBehindTheScenes": "Bastidores",
|
||||
"NameExtraClip": "Excerto",
|
||||
"NameExtraDeletedScene": "Cena eliminada",
|
||||
"NameExtraFeaturette": "Minidocumentário",
|
||||
"NameExtraInterview": "Entrevista",
|
||||
"NameExtraSample": "Amostra",
|
||||
"NameExtraShort": "Curta-metragem",
|
||||
"NameExtraThemeSong": "Tema musical",
|
||||
"NameExtraThemeVideo": "Vídeo temático",
|
||||
"NameExtraScene": "Cena",
|
||||
"NameExtraUnknown": "Extra",
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraNumbered": "{0} {1}"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,17 @@
|
||||
"CleanupUserDataTask": "Sarcina de curatare a datelor utilizatorului",
|
||||
"CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile.",
|
||||
"LyricDownloadFailureFromForItem": "Versurile nu au putut fi descărcate din {0} pentru {1}",
|
||||
"Original": "Original"
|
||||
"Original": "Original",
|
||||
"NameExtraBehindTheScenes": "În culise",
|
||||
"NameExtraClip": "Clip",
|
||||
"NameExtraDeletedScene": "Scenă ștearsă",
|
||||
"NameExtraFeaturette": "Material bonus",
|
||||
"NameExtraInterview": "Interviu",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Monstră",
|
||||
"NameExtraScene": "Scenă",
|
||||
"NameExtraShort": "Scurt",
|
||||
"NameExtraThemeSong": "Audio de Fundal",
|
||||
"NameExtraThemeVideo": "Video de Fundal",
|
||||
"NameExtraTrailer": "Trailer"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "Задача очистки пользовательских данных",
|
||||
"CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней.",
|
||||
"Original": "Оригинальный",
|
||||
"LyricDownloadFailureFromForItem": "Не получилось скачать текст песни с {0} для {1}"
|
||||
"LyricDownloadFailureFromForItem": "Не получилось скачать текст песни с {0} для {1}",
|
||||
"NameExtraBehindTheScenes": "За кадром",
|
||||
"NameExtraClip": "Отрывок",
|
||||
"NameExtraDeletedScene": "Удалённая сцена",
|
||||
"NameExtraFeaturette": "Короткометражка",
|
||||
"NameExtraInterview": "Интервью",
|
||||
"NameExtraSample": "Образец",
|
||||
"NameExtraScene": "Сцена",
|
||||
"NameExtraThemeSong": "Заглавная песня",
|
||||
"NameExtraThemeVideo": "Заглавное видео",
|
||||
"NameExtraTrailer": "Трейлер",
|
||||
"NameExtraUnknown": "Дополнительный материал",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraShort": "Короткометражка"
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"Default": "Predvolené",
|
||||
"TaskOptimizeDatabaseDescription": "Zmenší databázu a odstráni prázdne miesto. Spustenie tejto úlohy po skenovaní knižnice alebo po iných zmenách zahŕňajúcich úpravy databáze môže zlepšiť výkon.",
|
||||
"TaskOptimizeDatabase": "Optimalizovať databázu",
|
||||
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
|
||||
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z videosúborov na vytvorenie presnejších HLS zoznamov. Táto úloha môže trvať dlhší čas.",
|
||||
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
|
||||
"External": "Externé",
|
||||
"HearingImpaired": "Sluchovo postihnutí",
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTask": "Prečistiť používateľské dáta",
|
||||
"CleanupUserDataTaskDescription": "Vyčistí všetky dáta používateľa (stav sledovania, stav obľúbených atď.) z médií, ktoré už neexistujú aspoň 90 dní.",
|
||||
"LyricDownloadFailureFromForItem": "Text piesne sa nepodarilo stiahnuť z {0} pre {1}",
|
||||
"Original": "Originál"
|
||||
"Original": "Originál",
|
||||
"NameExtraBehindTheScenes": "Zo zákulisia",
|
||||
"NameExtraClip": "Klip",
|
||||
"NameExtraDeletedScene": "Vystrihnutá scéna",
|
||||
"NameExtraFeaturette": "Bonus",
|
||||
"NameExtraInterview": "Rozhovor",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Ukážka",
|
||||
"NameExtraScene": "Scéna",
|
||||
"NameExtraShort": "Krátky film",
|
||||
"NameExtraThemeSong": "Úvodná pieseň",
|
||||
"NameExtraThemeVideo": "Úvodné video",
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,17 @@
|
||||
"CleanupUserDataTask": "Čiščenje uporabniških podatkov",
|
||||
"CleanupUserDataTaskDescription": "Izbriše vse uporabniške podatke (stanje ogleda, priljubljene itd.) za vsebine, ki že več kot 90 dni niso na voljo.",
|
||||
"LyricDownloadFailureFromForItem": "Besedila ni bilo mogoče prenesti iz {0} za {1}",
|
||||
"Original": "Original"
|
||||
"Original": "Original",
|
||||
"NameExtraBehindTheScenes": "V zakulisju",
|
||||
"NameExtraClip": "Klip",
|
||||
"NameExtraDeletedScene": "Izbrisan prizor",
|
||||
"NameExtraFeaturette": "Kratek dokumentarec o izdelavi filma",
|
||||
"NameExtraInterview": "Intervju",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Vzorec",
|
||||
"NameExtraScene": "Prizor",
|
||||
"NameExtraShort": "Kratki film",
|
||||
"NameExtraThemeSong": "Tematska Pesem",
|
||||
"NameExtraThemeVideo": "Tematski Video",
|
||||
"NameExtraTrailer": "Napovednik"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Pastron të gjitha të dhënat e përdorueseve (gjendja e shikimit, statusi i të preferuarave etj.) nga mediat që nuk janë më të pranishme për të paktën 90 ditë.",
|
||||
"CleanupUserDataTask": "Veprim për pastrimin të dhënave të përdorueseve",
|
||||
"LyricDownloadFailureFromForItem": "Teksti i këngës nuk arriti të shkarkohej nga {0} për {1}",
|
||||
"Original": "Origjinal"
|
||||
"Original": "Origjinal",
|
||||
"NameExtraBehindTheScenes": "Pamje nga prapaskenat",
|
||||
"NameExtraClip": "Pjesë",
|
||||
"NameExtraDeletedScene": "Skenë e fshirë",
|
||||
"NameExtraFeaturette": "Film i shkurtër",
|
||||
"NameExtraInterview": "Intervistë",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Shembull",
|
||||
"NameExtraScene": "Skenë",
|
||||
"NameExtraShort": "Film i shkurtër",
|
||||
"NameExtraThemeSong": "Kënga e temës",
|
||||
"NameExtraThemeVideo": "Videoja e temës",
|
||||
"NameExtraTrailer": "Parapamje",
|
||||
"NameExtraUnknown": "Shtesë"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"TaskMoveTrickplayImages": "Промени локацију сличица за визуелно премотавање",
|
||||
"TaskDownloadMissingLyricsDescription": "Преузми стихове песама",
|
||||
"LyricDownloadFailureFromForItem": "Није успело преузимање стихова са {0} за {1}",
|
||||
"Original": "Изворно"
|
||||
"Original": "Изворно",
|
||||
"NameExtraBehindTheScenes": "Иза кулиса",
|
||||
"NameExtraClip": "Исечак",
|
||||
"NameExtraDeletedScene": "Обрисана сцена",
|
||||
"NameExtraFeaturette": "Кратки прилог",
|
||||
"NameExtraInterview": "Интервју",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "Узорак",
|
||||
"NameExtraScene": "Сцена",
|
||||
"NameExtraShort": "Кратки филм",
|
||||
"NameExtraThemeSong": "Музичка тема",
|
||||
"NameExtraThemeVideo": "Спот музичке теме",
|
||||
"NameExtraTrailer": "Реклама",
|
||||
"NameExtraUnknown": "Додатак"
|
||||
}
|
||||
|
||||
@@ -108,5 +108,18 @@
|
||||
"CleanupUserDataTaskDescription": "Tar bort all användardata (såsom vad du sett, favoriter med mera) för media som inte funnits på enheten på minst 90 dagar.",
|
||||
"CleanupUserDataTask": "Uppgift för rensning av användardata",
|
||||
"Original": "Original",
|
||||
"LyricDownloadFailureFromForItem": "Misslyckades att ladda ner låttexter från {0} för {1}"
|
||||
"LyricDownloadFailureFromForItem": "Misslyckades att ladda ner låttexter från {0} för {1}",
|
||||
"NameExtraBehindTheScenes": "Bakom kulisserna",
|
||||
"NameExtraDeletedScene": "Borttagen scen",
|
||||
"NameExtraInterview": "Intervju",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraScene": "Scen",
|
||||
"NameExtraShort": "Kortfilm",
|
||||
"NameExtraThemeSong": "Signaturmelodi",
|
||||
"NameExtraTrailer": "Trailer",
|
||||
"NameExtraClip": "Klipp",
|
||||
"NameExtraFeaturette": "Kortfilm",
|
||||
"NameExtraSample": "Prov",
|
||||
"NameExtraThemeVideo": "Signaturvideo",
|
||||
"NameExtraUnknown": "Extra"
|
||||
}
|
||||
|
||||
@@ -106,5 +106,20 @@
|
||||
"TaskExtractMediaSegments": "மீடியா பிரிவு ஸ்கேன்",
|
||||
"TaskExtractMediaSegmentsDescription": "மீடியாசெக்மென்ட் இயக்கப்பட்ட செருகுநிரல்களிலிருந்து மீடியா பிரிவுகளைப் பிரித்தெடுக்கிறது அல்லது பெறுகிறது.",
|
||||
"CleanupUserDataTaskDescription": "குறைந்தது 90 நாட்களுக்கு இல்லாத மீடியாவிலிருந்து அனைத்து பயனர் தரவையும் (கண்காணிப்பு நிலை, பிடித்த நிலை போன்றவை) சுத்தம் செய்கிறது.",
|
||||
"CleanupUserDataTask": "பயனர் தரவை சுத்தம் செய்யும் பணி"
|
||||
"CleanupUserDataTask": "பயனர் தரவை சுத்தம் செய்யும் பணி",
|
||||
"LyricDownloadFailureFromForItem": "{0} இலிருந்து {1} க்கு பாடல் வரிகளைப் பதிவிறக்க முடியவில்லை",
|
||||
"NameExtraBehindTheScenes": "திரைக்குப் பின்னால்",
|
||||
"NameExtraClip": "துண்டுக்காட்சி",
|
||||
"NameExtraDeletedScene": "நீக்கப்பட்ட காட்சிகள்",
|
||||
"NameExtraFeaturette": "திரைப்பின்னணித் தொகுப்பு",
|
||||
"NameExtraInterview": "நேர்காணல்",
|
||||
"NameExtraSample": "மாதிரி",
|
||||
"NameExtraScene": "காட்சி",
|
||||
"NameExtraShort": "குறுகிய",
|
||||
"NameExtraThemeSong": "மையப் பாடல்",
|
||||
"NameExtraThemeVideo": "மையக் காணொளி",
|
||||
"NameExtraTrailer": "முன்னோட்டம்",
|
||||
"NameExtraUnknown": "கூடுதல்",
|
||||
"Original": "அசல்",
|
||||
"NameExtraNumbered": "{0} {1}"
|
||||
}
|
||||
|
||||
@@ -22,20 +22,20 @@
|
||||
"NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว",
|
||||
"NameSeasonUnknown": "ไม่ทราบซีซัน",
|
||||
"NameSeasonNumber": "ซีซัน {0}",
|
||||
"NameInstallFailed": "การติดตั้ง {0} ล้มเหลว",
|
||||
"NameInstallFailed": "ติดตั้ง {0} ไม่สำเร็จ",
|
||||
"MusicVideos": "มิวสิควิดีโอ",
|
||||
"Music": "ดนตรี",
|
||||
"Music": "เพลง",
|
||||
"Movies": "ภาพยนตร์",
|
||||
"MixedContent": "เนื้อหาผสม",
|
||||
"Latest": "ล่าสุด",
|
||||
"LabelRunningTimeValue": "ผ่านไปแล้ว: {0}",
|
||||
"LabelIpAddressValue": "ที่อยู่ IP: {0}",
|
||||
"Inherit": "สืบทอด",
|
||||
"HomeVideos": "โฮมวิดีโอ",
|
||||
"HeaderNextUp": "ถัดไป",
|
||||
"HeaderLiveTV": "ทีวีสด",
|
||||
"HeaderFavoriteShows": "รายการที่ชื่นชอบ",
|
||||
"HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ",
|
||||
"MixedContent": "เนื้อหาหลากหลายประเภท",
|
||||
"Latest": "มาใหม่ล่าสุด",
|
||||
"LabelRunningTimeValue": "ความยาว: {0}",
|
||||
"LabelIpAddressValue": "หมายเลข IP: {0}",
|
||||
"Inherit": "ใช้ค่าเริ่มต้น",
|
||||
"HomeVideos": "วิดีโอส่วนตัว",
|
||||
"HeaderNextUp": "รายการถัดไป",
|
||||
"HeaderLiveTV": "ทีวีถ่ายทอดสด",
|
||||
"HeaderFavoriteShows": "รายการที่ชอบ",
|
||||
"HeaderFavoriteEpisodes": "ตอนที่ชอบ",
|
||||
"HeaderContinueWatching": "ดูต่อ",
|
||||
"Genres": "ประเภท",
|
||||
"Folders": "โฟลเดอร์",
|
||||
@@ -106,5 +106,20 @@
|
||||
"TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment",
|
||||
"TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay",
|
||||
"CleanupUserDataTask": "ส่วนงานล้างข้อมูลผู้ใช้",
|
||||
"CleanupUserDataTaskDescription": "ล้างข้อมูลผู้ใช้ทั้งหมด (สถานะการรับชม สถานะรายการโปรด ฯลฯ) จากสื่อที่ไม่ได้ใช้งานแล้วอย่างน้อย 90 วัน"
|
||||
"CleanupUserDataTaskDescription": "ล้างข้อมูลผู้ใช้ทั้งหมด (สถานะการรับชม สถานะรายการโปรด ฯลฯ) จากสื่อที่ไม่ได้ใช้งานแล้วอย่างน้อย 90 วัน",
|
||||
"LyricDownloadFailureFromForItem": "ดาวน์โหลดเนื้อเพลงจาก {0} สำหรับ {1} ไม่สำเร็จ",
|
||||
"Original": "ต้นฉบับ",
|
||||
"NameExtraBehindTheScenes": "เบื้องหลังการถ่ายทำ",
|
||||
"NameExtraClip": "คลิปวิดีโอ",
|
||||
"NameExtraDeletedScene": "ฉากที่ถูกตัดออก",
|
||||
"NameExtraFeaturette": "คลิปสั้นพิเศษ",
|
||||
"NameExtraInterview": "บทสัมภาษณ์",
|
||||
"NameExtraNumbered": "{0} {1}",
|
||||
"NameExtraSample": "ตัวอย่าง",
|
||||
"NameExtraScene": "ฉาก",
|
||||
"NameExtraShort": "ภาพยนตร์สั้น",
|
||||
"NameExtraThemeSong": "เพลงประกอบ",
|
||||
"NameExtraThemeVideo": "วิดีโอธีม",
|
||||
"NameExtraTrailer": "ตัวอย่างภาพยนตร์",
|
||||
"NameExtraUnknown": "เนื้อหาพิเศษ"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user