Compare commits
1569 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| ace30afcf8 | |||
| fc056b6273 | |||
| ac5efb4775 | |||
| fefd676adc | |||
| 59c17a663c | |||
| 641551e164 | |||
| bd543d7ac3 | |||
| 545e412259 | |||
| 7dff92bb82 | |||
| b36aab9399 | |||
| 2c7d2d4719 | |||
| 5c519270b8 | |||
| 55047b1183 | |||
| 794e1361d7 | |||
| 27c9c9c0ed | |||
| 68636b2390 | |||
| 2e6430c4f4 | |||
| c88d792963 | |||
| 73dbc9e89f | |||
| cf3edd9875 | |||
| ef0131ad69 | |||
| 056c318f04 | |||
| 49c3443b0c | |||
| e415718fe7 | |||
| 8abcfb2a80 | |||
| 9aadf97958 | |||
| 9e57121171 | |||
| b471811920 | |||
| 3cb99add76 | |||
| 001f1c4377 | |||
| 9ef3706b44 | |||
| 864d6d0b8f | |||
| a565e4896e | |||
| ceef9143ad | |||
| a7a92509c7 | |||
| e876e784da | |||
| 9b7d5edc86 | |||
| f01cddf273 | |||
| 0d4bd0495b | |||
| 6f9c4dea6e | |||
| 8c51920911 | |||
| 8f2fd65810 | |||
| 953659980f | |||
| 8ab1fecb70 | |||
| f5d42ee180 | |||
| e28d547006 | |||
| b3b9f74014 | |||
| 07d31c6ba5 | |||
| a9198e865e | |||
| 79ff0b0b00 | |||
| 2b45a984dd | |||
| 739642b330 | |||
| 6097045d71 | |||
| 51e20a14c2 | |||
| eb0d05cf1e | |||
| d3d5915f31 | |||
| 288640a5d0 | |||
| ff0a1b999f | |||
| da0fe7455e | |||
| bf69f9d8a8 | |||
| badf22fcc2 | |||
| b59e9f90f0 | |||
| 056b92dbd5 | |||
| ba80f5e416 | |||
| 97ec4c1da2 | |||
| 894ba1a410 | |||
| 0a0aaefad5 | |||
| c8b97bf533 | |||
| cfa4e357ea | |||
| 0f42aa892e | |||
| cce6bf27e0 | |||
| d6cebf1e67 | |||
| c053a6cd78 | |||
| d8c62420bf | |||
| d483c3efe6 | |||
| 275c1a3cc1 | |||
| 4942b2c15f | |||
| 3fc71293b4 | |||
| 8ea9bece03 | |||
| baa7f5f0b0 | |||
| b9c96f3d2c | |||
| 08f9b932ac | |||
| e6cd73df03 | |||
| 71ebb1f456 | |||
| 9c298c52f5 | |||
| 3e8db40901 | |||
| f9ead9615c | |||
| 93af2d6f67 | |||
| 027c91949d | |||
| 526ec83305 | |||
| dfcacce1b0 | |||
| 2a54669a8a | |||
| 54d48fa446 | |||
| 1736a566cc | |||
| 04ab362e59 | |||
| e282b05b8f | |||
| 2aa39226c6 | |||
| 60fbd39bb9 | |||
| 740b9924a0 | |||
| 5a6d9180fe | |||
| 897975fc57 | |||
| 7dab62616f | |||
| f1bd9a40d5 | |||
| 469e6e1bc8 | |||
| 38f5f8008a | |||
| 7bb68d8610 | |||
| 27047c35a4 | |||
| 42003ca9d2 | |||
| 98f5e21bb8 | |||
| 162985bb23 | |||
| 0d2c551cce | |||
| 717e7cbd77 | |||
| 58f9bdcf5c | |||
| 2a499aaa95 | |||
| 4246825239 | |||
| 68810c690b | |||
| b73ea1b99d | |||
| 59f77c24c9 | |||
| 0949212993 | |||
| 248aac9a3a | |||
| a1b85a63e7 | |||
| 091cb1c34a | |||
| eaf33f01e1 | |||
| db2dbaa62b | |||
| 1a7df6daf7 | |||
| a0b3e2b071 | |||
| 2618a5fba2 | |||
| 2ee887a502 | |||
| a17e157d44 | |||
| 6b6745b7fe | |||
| 594f9e4f6b | |||
| 4cda5f5ff2 | |||
| 24410d8a2e | |||
| 4d36bd635d | |||
| ef65534071 | |||
| 7c6cedd90a | |||
| 96590eea85 | |||
| 6796b3435d | |||
| 8776a447d1 | |||
| c02a24e32a | |||
| deee04ae38 | |||
| 580db0c1d2 | |||
| 8fcc2496d9 | |||
| f0e60a7ff3 | |||
| a99e67544a | |||
| bca6400bc3 | |||
| 986a509955 | |||
| da19f02f7b | |||
| 3fad5eb069 | |||
| 9923a51aed | |||
| 585e9a2fe2 | |||
| 8e81737dba | |||
| e4e578b37a | |||
| 387bc0c8eb | |||
| cbb569a277 | |||
| 1fa63b797b | |||
| aa3a7c88a4 | |||
| 0a2cf69a55 | |||
| 0845b0c258 | |||
| e043f93a72 | |||
| 6ac2d707cb | |||
| 20f7ddbf8f | |||
| 4849486fa0 | |||
| 4ccd3da77a | |||
| bc28dc11c0 | |||
| d9eaeed61d | |||
| c7320dc189 | |||
| 71048917dd | |||
| 11eab1b663 | |||
| a17a0495d8 | |||
| b3e57a5f7d | |||
| 65827cce6f | |||
| b5df0d2a34 | |||
| 339a31f0a5 | |||
| a0d4ae1974 | |||
| d65b18a7f3 | |||
| cc93b44947 | |||
| e753adac2c | |||
| 0b465842c8 | |||
| da3f3b09d9 | |||
| 7a9beb3745 | |||
| c7ee07b14a | |||
| d8dfbc26f6 | |||
| 88e0d35ed7 | |||
| 1eadb07a12 | |||
| 26d9633fed | |||
| 19aadd934b | |||
| ce28374d40 | |||
| 7aa1c46447 | |||
| ffb7753f8d | |||
| 14884f2628 | |||
| 41188ff054 | |||
| cb6e38d830 | |||
| 4ba34709d6 | |||
| 28b8d3ee29 | |||
| 9eaca73888 | |||
| 29e17b6bc0 | |||
| 84cde7383f | |||
| a2c0799489 | |||
| ad133eb6b9 | |||
| 50180adc53 | |||
| bd94ca3071 | |||
| 869b4f8bbf | |||
| a4d856360b | |||
| beca405ad4 | |||
| c0be325b89 | |||
| dea500b26b | |||
| 47634e731a | |||
| cd1d11366e | |||
| 76dfaead8b | |||
| 5eef85f027 | |||
| e6a7530ced | |||
| 00be664b9e | |||
| e1d0f7d1e5 | |||
| 0a4ff3f3c0 | |||
| 21f214b1a6 | |||
| 0650666497 | |||
| 877899dcc2 | |||
| bf2f8ec633 | |||
| 2eff03b03e | |||
| 103932e4fb | |||
| 2b94b3b5f6 | |||
| 64032e8656 | |||
| 329ce8d4c2 | |||
| 2a7c924904 | |||
| 72664a68bc | |||
| 3ec123b616 | |||
| 376220661b | |||
| 9e88121647 | |||
| c7c7b30d28 | |||
| 601ce4c3b1 | |||
| fcc7f53e81 | |||
| e3acf08acc | |||
| c60139a32c | |||
| 6d4efe6523 | |||
| 43a955dded | |||
| 5cae44fdf7 | |||
| c3cb5fd2f9 | |||
| 1262ac31dc | |||
| 0f5bb5cf76 | |||
| ce78af2ed4 | |||
| 4b6fb6c4bb | |||
| db7465e83d | |||
| 803e87ca5f | |||
| 9e36fa4263 | |||
| a52a230778 | |||
| b00e381109 | |||
| b8fb8bd608 | |||
| 34c9adef80 | |||
| c8d2f43660 | |||
| ef733c5ace | |||
| a1eb04dc0b | |||
| 711e649e35 | |||
| 1d408a1503 | |||
| 6391dd9570 | |||
| 2007815fa6 | |||
| a5b4eca804 | |||
| 76d498ac9d | |||
| 90b4345cfd | |||
| 317192c23d | |||
| dcb12a73fb | |||
| b15abddfd7 | |||
| cfde5af3b0 | |||
| 26a6cfaf65 | |||
| 8a8018f0de | |||
| 6f49782b7b | |||
| 536437bbe3 | |||
| ba54cda774 | |||
| e86315128d | |||
| 7320e10329 | |||
| 5b544bf1ed | |||
| 1a1d9b2404 | |||
| 96a05276a6 | |||
| dfab2fb6e2 | |||
| 7785b51f57 | |||
| a068f75623 | |||
| 1ed191c5b3 | |||
| 0e3fbb6abd | |||
| 583a861b32 | |||
| 3bcfe13652 | |||
| f5a135a1db | |||
| 0cea853b45 | |||
| 663087b155 | |||
| dddeea1f7b | |||
| a148a4ad02 | |||
| 57d077d08e | |||
| 774be151aa | |||
| 7569ac65a8 | |||
| 4621a99c7c | |||
| 1e796e0b7a | |||
| 4da5483ef4 | |||
| eea0872980 | |||
| 36c90ce2ce | |||
| 48e93dcbce | |||
| 6cee66119e | |||
| c62a07405e | |||
| 7bd08ab290 | |||
| 088ef0d37a | |||
| ba0f61ef2d | |||
| c70f6bffcf | |||
| 21a6d6f0d6 | |||
| aa77dfb92d | |||
| 2ad37fe021 | |||
| fd5205a6eb | |||
| 60cfa65cdc | |||
| e5139e1004 | |||
| aa1abf8b94 | |||
| 742b5637fa | |||
| 25a362345d | |||
| 310a54f090 | |||
| e9d92bdcb0 | |||
| dc39a51475 | |||
| c51f3a3342 | |||
| 7ece959f4e | |||
| c96e828002 | |||
| ab56ceaa16 | |||
| 4645633acf | |||
| d6f93759ea | |||
| bf3f37e3d0 | |||
| 982e0c9370 | |||
| 55e681b9a6 | |||
| 7ba77804c4 | |||
| af6f5a8ed0 | |||
| 1162fcebf8 | |||
| 38d0367c42 | |||
| 7d3372018f | |||
| 8629831658 | |||
| db55d983f8 | |||
| 4d5ba8d7a5 | |||
| 6d4169a449 | |||
| 8dcb0bfecb | |||
| 844d69ab64 | |||
| 5c36b44484 | |||
| 4e4d7e7764 | |||
| 4c268a3579 | |||
| 77bcd2f5f6 | |||
| 8406924471 | |||
| 67fd4ce187 | |||
| b37b39773a | |||
| 6f98767aed | |||
| 643460f484 | |||
| a4231bf428 | |||
| 9c817a97a9 | |||
| f9c4c9b345 | |||
| dde306b170 | |||
| e2b61d951b | |||
| 9eff25bfed | |||
| ff4484eb4a | |||
| 62b2adbf66 | |||
| 9ac8c2a2fa | |||
| 90e72fb687 | |||
| 630846798d | |||
| 9d5be19a27 | |||
| 6058ab50f8 | |||
| e3b379052d | |||
| 0b6f4b2bd9 | |||
| 4f6db1bc22 | |||
| 8c8c71125c | |||
| c6e568692e | |||
| d5a76bdff8 | |||
| ebdc756547 | |||
| 10d0cec7b9 | |||
| 10cc651790 | |||
| 7d18f3d6ed | |||
| 9b8c12d433 | |||
| ba0eb87371 | |||
| d561cef81f | |||
| b528c1100f | |||
| 96c9f4fdad | |||
| 6d077fcf40 | |||
| ab99b2bad3 | |||
| db36be7a6b | |||
| 85f158e1dd | |||
| e1365bd253 | |||
| 1ec66adc30 | |||
| af0bcbc652 | |||
| b2312466e1 | |||
| cc7915c2e6 | |||
| a537c66da1 | |||
| a43adf42f3 | |||
| 6996c8a1de | |||
| f976630003 | |||
| 965cf93419 | |||
| 70ea3f863a | |||
| 989aef18af | |||
| ccb917b8df | |||
| 7cf6389ab5 | |||
| 2473b89a8d | |||
| 6575c69a4e | |||
| 66d594836c | |||
| 43028f735f | |||
| e83b992eef | |||
| 8368d10d1b | |||
| e8291fc856 | |||
| 308707476d | |||
| e252589900 | |||
| 1220cac255 | |||
| 7218d82c21 | |||
| a4524eb2ad | |||
| 553ba56389 | |||
| afa2103d42 | |||
| 7256c9c89d | |||
| f3cdaeaa12 | |||
| 368808eba4 | |||
| 0fc8ed6aeb | |||
| f60281d8fd | |||
| 2936588c0f | |||
| 0e1be6ce30 | |||
| 4cd0a2ed8d | |||
| aa05185917 | |||
| 2d9257b203 | |||
| d1d9c8ed06 | |||
| 23c25289da | |||
| aad6bca955 | |||
| 9f0f9a276f | |||
| 6016159860 | |||
| 6ffc044af1 | |||
| c22f24319b | |||
| 1c4c9cf733 | |||
| ea34a38f09 | |||
| bbcfb2f421 | |||
| 0873fa8a86 | |||
| 9dc50b4ac6 | |||
| 617ab0d0ca | |||
| dee9629037 | |||
| 31f3b5f6bb | |||
| 2ac6a7ba3f | |||
| ece77779f8 | |||
| c15c1f82a3 | |||
| a15352b80c | |||
| 304b944152 | |||
| e81c8ac6d1 | |||
| 97c1cb2f26 | |||
| ac9d84f602 | |||
| f3bf3c9853 | |||
| 644245bb7c | |||
| a18c0007b4 | |||
| c8a51160b4 | |||
| 4a0a45a045 | |||
| 91da1c035d | |||
| 6b5ce934b3 | |||
| 7174bb6a93 | |||
| 7037121bd0 | |||
| 7417da0e5c | |||
| 1e8bf1ce8d | |||
| d4c3d24e52 | |||
| d3ad2aec60 | |||
| 3554f068fb | |||
| 6dac1fde0a | |||
| 56fe4a158e | |||
| c2332d340c | |||
| d5b5c71baf | |||
| 7aee5b1e70 | |||
| a8601b3797 | |||
| 1e9e4ffda9 | |||
| d7faf9a327 | |||
| bdb3adeb30 | |||
| 1f5cfb1e23 | |||
| c2cc27a8a9 | |||
| f4bce38ddb | |||
| 547b8cafb1 | |||
| 98daf4aedb | |||
| fcf56b73cb | |||
| e8239a7ee2 | |||
| 84cebeae64 | |||
| c0e2875818 | |||
| 411ba03bf0 | |||
| b2e19c0306 | |||
| 6d287d5627 | |||
| 4f3b49472a | |||
| ef90fe9157 | |||
| 97f9d0c7e6 | |||
| 9ab22e9f8b | |||
| 04422250eb | |||
| a702b62553 | |||
| 5d7ae6a134 | |||
| 5ac0260e5f | |||
| 88332e89c4 | |||
| a3578caa8c | |||
| b45e5463c1 | |||
| 10ab381e62 | |||
| fa363f6276 | |||
| 57214dafe8 | |||
| 9ed9347cde | |||
| 916e897ed2 | |||
| d5672ce407 | |||
| 0c46431cbb | |||
| e1a5c16404 | |||
| 44b5de1568 | |||
| 08b2ffeaab | |||
| 48825f468e | |||
| 56d6e4b4cc | |||
| 697bb6a480 | |||
| a1d72deba2 | |||
| 9456d7168f | |||
| ae5e6431c5 | |||
| e2ee58f29d | |||
| bd3ee9f1e4 | |||
| 7ef3f73ce4 | |||
| 4bc336091e | |||
| c75f1e2987 | |||
| 4a4ed1b5d5 | |||
| eea80e5735 | |||
| c565010f28 | |||
| 285cd708f6 | |||
| 3330465337 | |||
| 4ba95b9ac4 | |||
| 1ee705f3e1 | |||
| 0e0c7c5c59 | |||
| 4993234a7d | |||
| ed9affb14c | |||
| 41b9eb8c0a | |||
| 605b8bb7aa | |||
| 538d0b622a | |||
| 8cb077402f | |||
| 0cea039445 | |||
| c3762186da | |||
| 44981cd823 | |||
| 709d0bb231 | |||
| 56231222df | |||
| a6a89f7953 | |||
| 8339111732 | |||
| 87e50025a9 | |||
| 9d601f8e9b | |||
| fe2596dc0e | |||
| cdbf4752b9 | |||
| 43659f011c | |||
| 8d781a0966 | |||
| 7217b09cda | |||
| f577dd6d64 | |||
| 9afb441361 | |||
| 29fc7711a1 | |||
| 265cc50144 | |||
| 07b8247784 | |||
| 16e04970f7 | |||
| 61b928bc34 | |||
| 4014b262d9 | |||
| 8bc0123f42 | |||
| 28602af328 | |||
| 1b34ebc0fc | |||
| 14f142faa8 | |||
| fc2ecf9c93 | |||
| 86917cd167 | |||
| 6c57ad4772 | |||
| 140de04f94 | |||
| 49c6a99e00 | |||
| 07093c84c8 | |||
| a7bb3ea214 | |||
| 28e2f5bb08 | |||
| dec5814a6a | |||
| d976f13970 | |||
| 0c3ba30de2 | |||
| 4096c973c6 | |||
| ce0a6b2df8 | |||
| 06b7f88d6c | |||
| 5e5fdf6173 | |||
| 0cd039b65c | |||
| 3ec5b7efaa | |||
| 4199f7acc2 | |||
| 70dbf837c5 | |||
| 7e6d4424b2 | |||
| 930a4d0f60 | |||
| d894b91d46 | |||
| 57716833b8 | |||
| e5c40ea98d | |||
| b4a58ee13a | |||
| 2803d4941b | |||
| dbedb9096b | |||
| 3cf213c4fb | |||
| 3cc4e8a88a | |||
| 9050a094a7 | |||
| 5c9f70c375 | |||
| aa24d08d33 | |||
| a7891b3f2d | |||
| 0a0fcd3b19 | |||
| 055d466d16 | |||
| 5dc7962995 | |||
| e66c76fc34 | |||
| e57a95e9cd | |||
| 2b854fb365 | |||
| f436743f9f | |||
| 77bb1a726e | |||
| 6637102162 | |||
| 1c4b5199b8 | |||
| f576783ae1 | |||
| 67110b512a | |||
| 1264254301 | |||
| 2e467f29de | |||
| 9092130350 | |||
| a0b3b7335f | |||
| 5d65cfcd99 | |||
| 2ea7af777b | |||
| 8ee358de2c | |||
| df5671263f | |||
| f35b8dd33d | |||
| 8f1ebfc4cd | |||
| dbaa3e458d | |||
| d62cf7f7cb | |||
| cf0220f183 | |||
| 317d76e97c | |||
| c4cb41f3b2 | |||
| 32887adff3 | |||
| fca048fe18 | |||
| 1dd3808147 | |||
| a0931baa8e | |||
| 5e4bd744c0 | |||
| 576f6d411a | |||
| 51b54f5695 | |||
| 74230131a1 | |||
| 7df6e0b16f | |||
| 269508be9f | |||
| 1c190f7952 | |||
| e84826297d | |||
| 86b81c912d | |||
| ccc49b109f | |||
| 6e9e2f500f | |||
| 8be8ea60f1 | |||
| 22c816de0a | |||
| 61cb53999e | |||
| 5eefbb6bf6 | |||
| afdde7b243 | |||
| d6fbdcc0f8 | |||
| 5020c09640 | |||
| 874f6895a2 | |||
| c972047566 | |||
| dbf0edf4f8 | |||
| 4d7f85f14a | |||
| 9ec8790faa | |||
| 9a806cf3a4 | |||
| cad8de9701 | |||
| 294b2f90d1 | |||
| 32fe92d8f5 | |||
| c152f610ce | |||
| 0bbc6bb31d | |||
| cb59a017a5 | |||
| 070abcd8ff | |||
| 16dc1e2260 | |||
| 98697e75ca | |||
| 1e10cd003d | |||
| 5fc1b1c862 | |||
| 4fa1a9cb97 | |||
| 77ad7f6139 | |||
| 82a561b87d | |||
| 04ca27ad07 | |||
| e1ef4290af | |||
| 1ef0a41066 | |||
| b65e03da9a | |||
| fe79384cd5 | |||
| 2c9c9f591d | |||
| 7d705249ca | |||
| de3d1445c0 | |||
| 0e7ae0e9a4 | |||
| 2264d58ae7 | |||
| f7021d04eb | |||
| 1c2b48182a | |||
| d0c1ef8002 | |||
| d1ed6593ad | |||
| 596b635511 | |||
| 0bde7bae05 | |||
| a18d60d2de | |||
| 0573999d5e | |||
| 49ac705867 | |||
| 9c7cf808aa | |||
| 767ee2b5c4 | |||
| 086fbd49cf | |||
| 14b785d188 | |||
| 940c4e8ba8 | |||
| 2b742a5966 | |||
| 5769c398c6 | |||
| 4a4fef830e | |||
| e9729a536f | |||
| d9a79b5eef | |||
| 3fc3b04daf | |||
| 2ace880345 | |||
| d7b786e777 | |||
| 150094e3a4 | |||
| 824bafc32d | |||
| 90a6cca92b | |||
| 476a0d6932 | |||
| d75216cf3a | |||
| c906c1ca0d | |||
| c69e9d8f2c | |||
| 384134fd25 | |||
| 2c499d1e86 | |||
| 9657708b38 | |||
| cb931e0062 | |||
| 3df7d7a809 | |||
| 1fcc79316d | |||
| 6d7950bddc | |||
| a2ef0e4abe | |||
| 7f5cc544df | |||
| 15465afd8e | |||
| 6c46b06c75 | |||
| f02190c394 | |||
| 88ceaa39b0 | |||
| e9331fe9d7 | |||
| 9f70578997 | |||
| 07f07ba6bc | |||
| a123a2cb22 | |||
| 181a37a8cd | |||
| 7717d2e261 | |||
| b108a8cfc5 | |||
| ae4b35da46 | |||
| f6b98d0faf | |||
| 9e4abb7319 | |||
| d06ce1f1e0 | |||
| cafb7cd002 | |||
| 777e0823ba | |||
| 296b17bf44 | |||
| 08dbb5c842 | |||
| d848faeb75 | |||
| 1b388d7296 | |||
| bfff1b9be2 | |||
| 42bdb22bfb | |||
| 160020c551 | |||
| 7cd059c033 | |||
| 850f1c79f1 | |||
| 035ecbdde3 | |||
| 2c0ecd6775 | |||
| cd5f18a084 | |||
| daf8eca8ae | |||
| 8680170706 | |||
| 480244e111 | |||
| 64a5a8419d | |||
| 592f278ee2 | |||
| ef7f6fc8a9 | |||
| 8d49a396e8 | |||
| 8e9b57aea9 | |||
| ea8f1ffb7c | |||
| e4b11c664c | |||
| a026a3722c | |||
| aa4936c59c | |||
| 3c2d3ac18b | |||
| 671d801d9f | |||
| 516754c2a6 | |||
| ea6130b354 | |||
| b3b2da681f | |||
| 35f8720251 | |||
| dfb485d1f2 | |||
| 8db6a39e92 | |||
| 8b6aec7ce5 | |||
| c77a0719c2 | |||
| 350983e03c | |||
| aabaf1a656 | |||
| 69b07c9f31 | |||
| c6178c63bf | |||
| 3eca221cc6 | |||
| 11fbca45ff | |||
| c24d0c1240 | |||
| 85b5bebda4 | |||
| e1392ca1b6 | |||
| 62fc2b8d0d | |||
| 2d9549dbbc | |||
| 747fa4699a | |||
| 407935d181 | |||
| 6104d8d5f9 | |||
| 8a6d1402d2 | |||
| e684f26c97 | |||
| cf1f251f2a | |||
| 0eed5ee79b | |||
| 7d6bf5cb0d | |||
| 14e3b2214a | |||
| b346d12e1c | |||
| 79437f85c5 | |||
| 8cb5ea60d6 | |||
| cbca153132 | |||
| f87150bb3d | |||
| a5f3d942f6 | |||
| 237e7bd44b | |||
| 28fc0e4796 | |||
| 490e087b46 | |||
| 4325c67e89 | |||
| f1dd065eca | |||
| de5b6470be | |||
| 6331de2e13 | |||
| 9c5a304142 | |||
| ea8be12dea | |||
| ab99572eb2 | |||
| 0d7eb48930 | |||
| 8ef7b4f9b5 | |||
| f5adbc0296 | |||
| cb650c69b8 | |||
| 70b8fa73f0 | |||
| 7abb94d8a2 | |||
| e137a06362 | |||
| 4e3d7383f5 | |||
| ab369f27f7 | |||
| 728819780a | |||
| efb901c369 | |||
| aad7506e85 | |||
| d116f989a8 | |||
| 82b3135dd9 | |||
| a8d9607298 | |||
| feea5af2f3 | |||
| a6b4d124d7 | |||
| 04f7cd6011 | |||
| 710c253318 | |||
| f035b11625 | |||
| 93dd5551df | |||
| f6603018d6 | |||
| 0803600afd | |||
| c38e887ea5 | |||
| 1131b051d8 | |||
| 33e8c18136 | |||
| 06be4998e1 | |||
| d28ee96f06 | |||
| 114591c1aa | |||
| e7bc86ebb8 | |||
| 068bc68764 | |||
| 7f8eb179a6 | |||
| 7aa96dfc20 | |||
| 260f1323d8 | |||
| 10c6266989 | |||
| d18066f0f2 | |||
| 83b2c47237 | |||
| 70d07b830d | |||
| a4aefc8a80 | |||
| a9f84b92df | |||
| 8c0b0d9102 | |||
| 963f2357a9 | |||
| 7735aafef5 | |||
| a05b3be1b3 | |||
| 5ff2767012 | |||
| 7ca09c4081 | |||
| 05f5d19ff4 | |||
| 51e0ce7ea4 | |||
| 3e223ead1e | |||
| 69e3e4c468 | |||
| 44dfe554a8 | |||
| 0dbd875dd0 | |||
| 5303445c9b | |||
| f07e1f4aae | |||
| d8030147ff | |||
| ddc20b74bf | |||
| 8b07c1f53d | |||
| a085b90e05 | |||
| 712908d53c | |||
| 523123dd36 | |||
| 03a2b2f2e8 | |||
| 06527fae6e | |||
| 66e571cd97 | |||
| 84450bb297 | |||
| b5fcbfc15e | |||
| 237c1d9b97 | |||
| a0ab0eb875 | |||
| 2db0750abb | |||
| fb69b976bf | |||
| fa97e8e183 | |||
| debc499711 | |||
| b2a2fd6fcc | |||
| 475bfd3e32 | |||
| fb9f983d20 | |||
| 1ebef57508 | |||
| 17e78c0d40 | |||
| dd7a804cfb | |||
| d2e7ab1c1a | |||
| f12acb014a | |||
| b91f63ce8b | |||
| 2b5cb5f9f4 | |||
| dc654065f2 | |||
| ccaaaca712 | |||
| ce76817020 | |||
| c9c90050d9 | |||
| 9e13003fbb | |||
| ba46608ffe | |||
| b6dad55ad3 | |||
| 6922fd0a38 | |||
| 7a5a4ad7da | |||
| 075fec6fc6 | |||
| 75c0a7a107 | |||
| e1dd2dce92 | |||
| b11de39c34 | |||
| f4a2679177 | |||
| db2167178a | |||
| 69784b2f17 | |||
| 83f0f3d629 | |||
| 3a4d67319a | |||
| 7f41cc53ca | |||
| 0a4ca33d4f | |||
| 341bb02422 | |||
| 4e64b261a8 | |||
| ead7de18df | |||
| dfdef511a5 | |||
| 00b66a06ea | |||
| 9aec576c76 | |||
| 2de04cb07c | |||
| e7f32fb174 | |||
| 1acefa6182 | |||
| 533ceeaaf2 | |||
| b0e853070b | |||
| e8cbcde02e | |||
| d376b5fbc7 | |||
| c77b3fa258 | |||
| 10f4f8b2ab | |||
| c05a41cc3c | |||
| 24be951b75 | |||
| dc28056450 | |||
| abdb5ab79e | |||
| eb4162f9ec | |||
| 608c44d5b3 | |||
| ceba3475fb | |||
| d52ab30ae9 | |||
| e79fc6b851 | |||
| 4595625f19 | |||
| c44006c20d | |||
| eac491fbd3 | |||
| 8cb11692a9 | |||
| 533464e186 | |||
| 2392290b72 | |||
| 144e62027d | |||
| 4c17498369 | |||
| b79f96e98b | |||
| f46cb112f7 | |||
| bc1419728f | |||
| 39cd3dcbd1 | |||
| 51207edf44 | |||
| 078587d232 | |||
| df8f352d65 | |||
| c9237ae731 | |||
| cfeb879519 | |||
| efb402b1d2 | |||
| 61b2ad7f49 | |||
| 2e5ff6842a | |||
| 4b57f2bdbb | |||
| 17003f4d76 | |||
| ebe89c07b3 | |||
| 6a757ac0e5 | |||
| ce64dbc034 | |||
| 4fa2f2475c | |||
| 379a104cfb | |||
| d583d9a313 | |||
| 6fda268892 | |||
| 350b7feefa | |||
| 9734892322 | |||
| bcdffa74a8 | |||
| 0869a4f1f6 | |||
| 40da2ccac5 | |||
| e806fec902 | |||
| 44173cc802 | |||
| d07e1a13b3 | |||
| 74858042fc | |||
| 4ce0d498ab | |||
| ce00bc076e | |||
| 433640d985 | |||
| 844646e2fe | |||
| 9d1c4ea169 | |||
| 2e080087e6 | |||
| 7684986fa1 | |||
| aa811eb1e3 | |||
| 7e9ce78849 | |||
| b37bc9016f | |||
| 0541808c25 | |||
| ab124bec88 | |||
| 8b180aca3a | |||
| 6454a35ef8 | |||
| c877ffa5ad | |||
| 044cf9fb85 | |||
| cace593472 | |||
| b318f33599 | |||
| cc284afb47 | |||
| 93b8eade61 | |||
| 4cdb2c7cfa | |||
| 679ee960d3 | |||
| 07dce33452 | |||
| 8c01b64a83 | |||
| a70200af14 | |||
| 87432e2368 | |||
| 45412639fa | |||
| 724d9c18f7 | |||
| 13c9880100 | |||
| cd2255a3ad | |||
| e7c130abcf | |||
| 579b0f6565 | |||
| 3aa1ebb500 | |||
| 47f798827b | |||
| 5612d2187b | |||
| 64cf67f1ac | |||
| caa1a06dec | |||
| 48ae3bc0df | |||
| 7d1a9dcc61 | |||
| cd75df6521 | |||
| 56a4aa180b | |||
| 2f306358c0 | |||
| b908fb5788 | |||
| 96e4d8ca78 | |||
| 294190bb21 | |||
| a7ad5dfc80 | |||
| b33810534b | |||
| 2624021d67 | |||
| 37e6ed5feb | |||
| c166387a5a | |||
| 3cb7c48f85 | |||
| 5b962534be | |||
| 04fe74ce8f | |||
| 85e0cad5f3 | |||
| 0409849cc7 | |||
| 5a02ea4a31 | |||
| fdb489ae47 | |||
| 6282455ff0 | |||
| 4c1a47bc53 | |||
| d716a53ec2 | |||
| fb5da641f4 | |||
| fd3057b549 | |||
| 3b8e614819 | |||
| 17bbe4a2cd | |||
| 42c1d7a915 | |||
| 8cf1a50b2e | |||
| 25ef02d8df | |||
| ac9bafa7e7 | |||
| a4e41c751a | |||
| 878e778fbc | |||
| 5b63d093b1 | |||
| 814264f62c | |||
| 649d41914f | |||
| b077d378fb | |||
| d22094be03 | |||
| 4e87af6d03 | |||
| 07185bc32b | |||
| ab38009069 | |||
| c07bce97a1 | |||
| e16ea7b236 | |||
| 02358a662e | |||
| b5b7bd2959 | |||
| b07231ae33 | |||
| 0d8b387e71 | |||
| 80d98379de | |||
| a9d299253e | |||
| c966ad906c | |||
| 50463d2d17 | |||
| 5774b601f5 | |||
| 4cbdb01c3d | |||
| c8c544dc5e | |||
| c0fd8dab22 | |||
| b73e3637de | |||
| f8e2b866b3 | |||
| 9ed55affa6 | |||
| 00163ce167 | |||
| 87612ef20d | |||
| c4d4419800 | |||
| 17f0643147 | |||
| 170368f9c9 | |||
| ea0a78dd0b | |||
| dcfbf55794 | |||
| 73a1259382 | |||
| 695aa594f2 | |||
| 1143d9509f | |||
| a0c568bc6c | |||
| 1c3196dd5f | |||
| 55f5eaf0e3 | |||
| 6a91c80f12 | |||
| 23cf4bc94e | |||
| fa5b8ee863 | |||
| e0d563782e | |||
| 2614fecf8d | |||
| b89877554c | |||
| 623d0cd8ad | |||
| 9322e37b95 | |||
| 82cbd01354 | |||
| 6d74b97836 | |||
| 4b11cad6d4 | |||
| 08027b1008 | |||
| 0fc288936d | |||
| 692e7bd4c4 | |||
| 6691380c04 | |||
| f9aa93406b | |||
| 88b8a13ecd | |||
| fe1aab034e | |||
| 43fff5799b | |||
| 630de12e5e | |||
| 1feceea508 | |||
| bd1eee5e27 | |||
| 37f7bda3cc | |||
| 3d819b74bd | |||
| 12c14ddb24 | |||
| a7e5f43a8a | |||
| f1e020c0b0 | |||
| 06923cbf2b | |||
| b8c7cd5aa7 | |||
| 9ae1ac2513 | |||
| b7f6ccc306 | |||
| 5cbe71a1b2 | |||
| ea45804213 | |||
| a1e635809b | |||
| e922fe8582 | |||
| 556f4c4bfb | |||
| b03f478867 | |||
| 1272bb9a84 | |||
| 572c5e24fa | |||
| 9fef0b7e5f | |||
| a0746c9a46 | |||
| 1399e6be38 | |||
| 1c77e9606e | |||
| 4254c5acf6 | |||
| e688e3f1da | |||
| cd6f52a29f | |||
| 3b18a36ba5 | |||
| 6a08361f6f | |||
| 80cace4321 | |||
| edb30ee543 | |||
| f932c4efa7 | |||
| 1ba0b88703 | |||
| 0dd6dacc4f | |||
| 06c603428b | |||
| 924c80a209 | |||
| 7f81bbd42f | |||
| 9f86f8748c | |||
| 7f296d06e6 | |||
| fbdbf77a59 | |||
| 661caa62e2 | |||
| 87a3c5d11c | |||
| 547d393af0 | |||
| 96cf13060d | |||
| ee66c74527 | |||
| 882f3374ed | |||
| 19c5c95f4e | |||
| 4f562d67b0 | |||
| 6e7118eff1 | |||
| 27b044493a | |||
| 2a8ebccd16 | |||
| d1d5ea9c80 | |||
| 136a7995f7 | |||
| 4c65e0d397 | |||
| e8761044c2 | |||
| 25f8e2259a | |||
| fa5faa09c0 | |||
| cf79b072a7 | |||
| 68252ffa1b | |||
| c71dc380bf | |||
| b39553611d | |||
| 427359deee | |||
| 17e4485b94 | |||
| b6177363e9 | |||
| 9f7f9cc0ff | |||
| 9e05abcc85 | |||
| ceb850c770 | |||
| c925f8688e | |||
| 2831882054 | |||
| 5e8c0fe40c | |||
| 3a8b27eb1e | |||
| 33c253268e | |||
| 6e022465e9 | |||
| 77bae62acc | |||
| 1be18114a9 | |||
| ae721542cc | |||
| 3eedbae506 | |||
| 03e08412d7 | |||
| 19e55f4309 | |||
| 92eb983c61 | |||
| 0ff1ee951d | |||
| 37129f7952 | |||
| d4aca84581 | |||
| e8be7ab011 | |||
| 30ba35aa0c | |||
| b60cd378d9 | |||
| 060aa4719e | |||
| 96d9bb83a3 | |||
| b830c42fca | |||
| 023838f3c8 | |||
| 75d40e69b5 | |||
| f81d124019 | |||
| 5167333602 | |||
| 93adddd7a9 | |||
| aea255f910 | |||
| 432cfba2e2 | |||
| c5488f8ead | |||
| 7d137a8e8a | |||
| 056dcf7e81 | |||
| 5f2be93e19 | |||
| 6bcc7aa79f | |||
| ffc18a2044 | |||
| a71187ebcc | |||
| 7c51b37ca0 | |||
| 6b371ba04f | |||
| e43e34eab8 | |||
| 2060d0ca2c | |||
| 16bc1ebc8b | |||
| 53683809d9 | |||
| 7b81a39ee1 | |||
| fcb1dfc010 | |||
| 5fb4d6a169 | |||
| 4747edd635 | |||
| 11388c0144 | |||
| cafc74c64c | |||
| c7f63a0da1 | |||
| 07455dfb4d | |||
| 3b8e177ba8 | |||
| 8165813414 | |||
| acd878e67e | |||
| b744ceabaa | |||
| d073e2c664 | |||
| 46905ac66a | |||
| dc90b0edb9 | |||
| 22515ad647 | |||
| d3174b5171 | |||
| 85b8b2573b | |||
| a7a2257ccb | |||
| 510b29f2a4 | |||
| 817ca1775a | |||
| 00c4f23276 | |||
| 43a2ec990c | |||
| efe5b59517 | |||
| 508b27f156 | |||
| bdab5e549e | |||
| 8f3410c81c | |||
| 2d28b2ff6e | |||
| 772e9a6d4a | |||
| a5e05a7f14 | |||
| ceefb71fe9 | |||
| 741397f1be | |||
| c6e67edd86 | |||
| fb48d0790f | |||
| 8dbbb3e243 | |||
| 2d4f7f725f | |||
| 911139e2d5 | |||
| 6b777f9d43 | |||
| 67d8e8c7da | |||
| efc6611072 | |||
| fb88d48374 | |||
| dfbbbf023d | |||
| 4b0a5ea8e9 | |||
| 4959232b27 | |||
| 73ddbeb4c1 | |||
| b5bb2261bc | |||
| cec4ad9b65 | |||
| b0b14e6edd | |||
| 6efcd6b873 | |||
| fabf616a5a | |||
| 9ee8642813 | |||
| 71daa3e5a1 | |||
| dacb407bec | |||
| 257d8d12b8 | |||
| bf00899f92 | |||
| e34ea6400b | |||
| 5776163d6e | |||
| 1dd3792984 | |||
| 46fb6c1579 | |||
| 9e386ecc27 | |||
| d81fec6b7c | |||
| 510312045a | |||
| 600a09f1fc | |||
| 725c414682 | |||
| a416c438da | |||
| 30e20a0146 | |||
| 66aad36d1f | |||
| 258ae9d4c2 | |||
| d2db700402 | |||
| 282784cbb6 | |||
| 9259623abb | |||
| d4e8c641ea | |||
| 0639758abd | |||
| f80fa96453 | |||
| 76df4c48bc | |||
| 9342a6a9d6 | |||
| 0bce3500fb | |||
| 12db844d14 | |||
| 6392970066 | |||
| a3ae055779 | |||
| 07ed9a3ea4 | |||
| 3d87d0faa2 | |||
| 0437511931 | |||
| 9d676f8836 | |||
| f0700b76d5 | |||
| 6b135ea209 | |||
| a0c634a6ed | |||
| 41c27d4e7e | |||
| dc029d549c | |||
| ebabaac6b1 | |||
| cd81a698a6 | |||
| 1e7acec017 | |||
| d6d6ebe3fb | |||
| 6f268736f0 | |||
| 421b49dee9 | |||
| a9f387f19b | |||
| bf7e6858d5 | |||
| c2a0dfb1e5 | |||
| 447ff1d23c | |||
| e331dc35ac | |||
| d4ca8d58c4 | |||
| 10a2a316a4 | |||
| cd2e043472 | |||
| 5957790ce8 | |||
| 79ee36ee15 | |||
| e20ecfc670 | |||
| 058a567e00 | |||
| 05ffa7b413 | |||
| b73985e04f | |||
| f397fc5b98 | |||
| ae641b7f3a | |||
| 9c5599f81b | |||
| 439a997fca | |||
| ea4c208fde | |||
| 5e922f1c10 | |||
| 441b995189 | |||
| f58a24f005 | |||
| ee0dad6f43 | |||
| 3e7ce5e1df | |||
| 7f03f39bcc | |||
| 7a5c7e70f6 | |||
| 868bb9ea25 | |||
| 2c2e33dd82 | |||
| fe9c96d052 | |||
| 4c86642c00 | |||
| 2955f2f562 | |||
| eb601e944c | |||
| f1ae764041 | |||
| 473628ba3a | |||
| 5267851e64 | |||
| 01d834f21a | |||
| c2844bda3b | |||
| 3dc4024338 | |||
| 2014fa56b8 | |||
| b09a41ad1f | |||
| be48cdd9e9 | |||
| 15bf43e3ad | |||
| 6acd146d17 | |||
| ea81db67f4 | |||
| 90103165e2 | |||
| 527998cd0c | |||
| d5409a26ea | |||
| 6c819fe516 | |||
| d3a3d9fce3 | |||
| 6dc61a430b | |||
| ee1bdf4e22 | |||
| d0b4b2ddb3 |
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "8.0.8",
|
"version": "9.0.11",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Development Jellyfin Server - FFmpeg",
|
|
||||||
"image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
|
|
||||||
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
|
|
||||||
"postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust; sudo bash \"./.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh\"",
|
|
||||||
// reads the extensions list and installs them
|
|
||||||
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/dotnet:2": {
|
|
||||||
"version": "none",
|
|
||||||
"dotnetRuntimeVersions": "8.0",
|
|
||||||
"aspNetCoreRuntimeVersions": "8.0"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
|
|
||||||
"preserve_apt_list": false,
|
|
||||||
"packages": ["libfontconfig1"]
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
|
||||||
"dockerDashComposeVersion": "v2"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
|
||||||
"ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}
|
|
||||||
},
|
|
||||||
"hostRequirements": {
|
|
||||||
"memory": "8gb",
|
|
||||||
"cpus": 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "Development Jellyfin Server",
|
"name": "Development Jellyfin Server",
|
||||||
"image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
|
"image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
|
||||||
|
"service": "app",
|
||||||
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
|
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
|
||||||
"postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust",
|
"postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
|
||||||
// reads the extensions list and installs them
|
// reads the extensions list and installs them
|
||||||
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
|
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/dotnet:2": {
|
"ghcr.io/devcontainers/features/dotnet:2": {
|
||||||
"version": "none",
|
"version": "none",
|
||||||
"dotnetRuntimeVersions": "8.0",
|
"dotnetRuntimeVersions": "9.0",
|
||||||
"aspNetCoreRuntimeVersions": "8.0"
|
"aspNetCoreRuntimeVersions": "9.0"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
|
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
|
||||||
"preserve_apt_list": false,
|
"preserve_apt_list": false,
|
||||||
"packages": ["libfontconfig1"]
|
"packages": [
|
||||||
|
"libfontconfig1"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||||
"dockerDashComposeVersion": "v2"
|
"dockerDashComposeVersion": "v2"
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
## configure the following for a manuall install of a specific version from the repo
|
## configure the following for a manual install of a specific version from the repo
|
||||||
|
|
||||||
# wget https://repo.jellyfin.org/releases/server/ubuntu/versions/jellyfin-ffmpeg/6.0.1-1/jellyfin-ffmpeg6_6.0.1-1-jammy_amd64.deb -O ffmpeg.deb
|
# wget https://repo.jellyfin.org/releases/server/ubuntu/versions/jellyfin-ffmpeg/6.0.1-1/jellyfin-ffmpeg6_6.0.1-1-jammy_amd64.deb -O ffmpeg.deb
|
||||||
|
|
||||||
@@ -29,4 +29,4 @@ Signed-By: /etc/apt/keyrings/jellyfin.gpg
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
sudo apt update -y
|
sudo apt update -y
|
||||||
sudo apt install jellyfin-ffmpeg6 -y
|
sudo apt install jellyfin-ffmpeg7 -y
|
||||||
+341
@@ -192,3 +192,344 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
|||||||
# Wrapping preferences
|
# Wrapping preferences
|
||||||
csharp_preserve_single_line_statements = true
|
csharp_preserve_single_line_statements = true
|
||||||
csharp_preserve_single_line_blocks = true
|
csharp_preserve_single_line_blocks = true
|
||||||
|
|
||||||
|
###############################
|
||||||
|
# C# Analyzer Rules #
|
||||||
|
###############################
|
||||||
|
### ERROR #
|
||||||
|
###########
|
||||||
|
# error on SA1000: The keyword 'new' should be followed by a space
|
||||||
|
dotnet_diagnostic.SA1000.severity = error
|
||||||
|
|
||||||
|
# error on SA1001: Commas should not be preceded by whitespace
|
||||||
|
dotnet_diagnostic.SA1001.severity = error
|
||||||
|
|
||||||
|
# error on SA1106: Code should not contain empty statements
|
||||||
|
dotnet_diagnostic.SA1106.severity = error
|
||||||
|
|
||||||
|
# error on SA1107: Code should not contain multiple statements on one line
|
||||||
|
dotnet_diagnostic.SA1107.severity = error
|
||||||
|
|
||||||
|
# error on SA1028: Code should not contain trailing whitespace
|
||||||
|
dotnet_diagnostic.SA1028.severity = error
|
||||||
|
|
||||||
|
# error on SA1117: The parameters should all be placed on the same line or each parameter should be placed on its own line
|
||||||
|
dotnet_diagnostic.SA1117.severity = error
|
||||||
|
|
||||||
|
# error on SA1137: Elements should have the same indentation
|
||||||
|
dotnet_diagnostic.SA1137.severity = error
|
||||||
|
|
||||||
|
# error on SA1142: Refer to tuple fields by name
|
||||||
|
dotnet_diagnostic.SA1142.severity = error
|
||||||
|
|
||||||
|
# error on SA1210: Using directives should be ordered alphabetically by the namespaces
|
||||||
|
dotnet_diagnostic.SA1210.severity = error
|
||||||
|
|
||||||
|
# error on SA1316: Tuple element names should use correct casing
|
||||||
|
dotnet_diagnostic.SA1316.severity = error
|
||||||
|
|
||||||
|
# error on SA1414: Tuple types in signatures should have element names
|
||||||
|
dotnet_diagnostic.SA1414.severity = error
|
||||||
|
|
||||||
|
# disable warning SA1513: Closing brace should be followed by blank line
|
||||||
|
dotnet_diagnostic.SA1513.severity = error
|
||||||
|
|
||||||
|
# error on SA1518: File is required to end with a single newline character
|
||||||
|
dotnet_diagnostic.SA1518.severity = error
|
||||||
|
|
||||||
|
# error on SA1629: Documentation text should end with a period
|
||||||
|
dotnet_diagnostic.SA1629.severity = error
|
||||||
|
|
||||||
|
# error on CA1001: Types that own disposable fields should be disposable
|
||||||
|
dotnet_diagnostic.CA1001.severity = error
|
||||||
|
|
||||||
|
# error on CA1012: Abstract types should not have public constructors
|
||||||
|
dotnet_diagnostic.CA1012.severity = error
|
||||||
|
|
||||||
|
# error on CA1063: Implement IDisposable correctly
|
||||||
|
dotnet_diagnostic.CA1063.severity = error
|
||||||
|
|
||||||
|
# error on CA1305: Specify IFormatProvider
|
||||||
|
dotnet_diagnostic.CA1305.severity = error
|
||||||
|
|
||||||
|
# error on CA1307: Specify StringComparison for clarity
|
||||||
|
dotnet_diagnostic.CA1307.severity = error
|
||||||
|
|
||||||
|
# error on CA1309: Use ordinal StringComparison
|
||||||
|
dotnet_diagnostic.CA1309.severity = error
|
||||||
|
|
||||||
|
# error on CA1310: Specify StringComparison for correctness
|
||||||
|
dotnet_diagnostic.CA1310.severity = error
|
||||||
|
|
||||||
|
# error on CA1513: Use 'ObjectDisposedException.ThrowIf' instead of explicitly throwing a new exception instance
|
||||||
|
dotnet_diagnostic.CA1513.severity = error
|
||||||
|
|
||||||
|
# error on CA1725: Parameter names should match base declaration
|
||||||
|
dotnet_diagnostic.CA1725.severity = error
|
||||||
|
|
||||||
|
# error on CA1725: Call async methods when in an async method
|
||||||
|
dotnet_diagnostic.CA1727.severity = error
|
||||||
|
|
||||||
|
# error on CA1813: Avoid unsealed attributes
|
||||||
|
dotnet_diagnostic.CA1813.severity = error
|
||||||
|
|
||||||
|
# error on CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string
|
||||||
|
dotnet_diagnostic.CA1834.severity = error
|
||||||
|
|
||||||
|
# error on CA1843: Do not use 'WaitAll' with a single task
|
||||||
|
dotnet_diagnostic.CA1843.severity = error
|
||||||
|
|
||||||
|
# error on CA1845: Use span-based 'string.Concat'
|
||||||
|
dotnet_diagnostic.CA1845.severity = error
|
||||||
|
|
||||||
|
# error on CA1849: Call async methods when in an async method
|
||||||
|
dotnet_diagnostic.CA1849.severity = error
|
||||||
|
|
||||||
|
# error on CA1851: Possible multiple enumerations of IEnumerable collection
|
||||||
|
dotnet_diagnostic.CA1851.severity = error
|
||||||
|
|
||||||
|
# error on CA1854: Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup
|
||||||
|
dotnet_diagnostic.CA1854.severity = error
|
||||||
|
|
||||||
|
# error on CA1860: Avoid using 'Enumerable.Any()' extension method
|
||||||
|
dotnet_diagnostic.CA1860.severity = error
|
||||||
|
|
||||||
|
# error on CA1861: Avoid constant arrays as arguments
|
||||||
|
dotnet_diagnostic.CA1861.severity = error
|
||||||
|
|
||||||
|
# error on CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
|
||||||
|
dotnet_diagnostic.CA1862.severity = error
|
||||||
|
|
||||||
|
# error on CA1863: Use 'CompositeFormat'
|
||||||
|
dotnet_diagnostic.CA1863.severity = error
|
||||||
|
|
||||||
|
# error on CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method
|
||||||
|
dotnet_diagnostic.CA1864.severity = error
|
||||||
|
|
||||||
|
# error on CA1865-CA1867: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char
|
||||||
|
dotnet_diagnostic.CA1865.severity = error
|
||||||
|
dotnet_diagnostic.CA1866.severity = error
|
||||||
|
dotnet_diagnostic.CA1867.severity = error
|
||||||
|
|
||||||
|
# error on CA1868: Unnecessary call to 'Contains' for sets
|
||||||
|
dotnet_diagnostic.CA1868.severity = error
|
||||||
|
|
||||||
|
# error on CA1869: Cache and reuse 'JsonSerializerOptions' instances
|
||||||
|
dotnet_diagnostic.CA1869.severity = error
|
||||||
|
|
||||||
|
# error on CA1870: Use a cached 'SearchValues' instance
|
||||||
|
dotnet_diagnostic.CA1870.severity = error
|
||||||
|
|
||||||
|
# error on CA1871: Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull'
|
||||||
|
dotnet_diagnostic.CA1871.severity = error
|
||||||
|
|
||||||
|
# error on CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'
|
||||||
|
dotnet_diagnostic.CA1872.severity = error
|
||||||
|
|
||||||
|
# error on CA2016: Forward the CancellationToken parameter to methods that take one
|
||||||
|
# or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token
|
||||||
|
dotnet_diagnostic.CA2016.severity = error
|
||||||
|
|
||||||
|
# error on CA2201: Exception type System.Exception is not sufficiently specific
|
||||||
|
dotnet_diagnostic.CA2201.severity = error
|
||||||
|
|
||||||
|
# error on CA2215: Dispose methods should call base class dispose
|
||||||
|
dotnet_diagnostic.CA2215.severity = error
|
||||||
|
|
||||||
|
# error on CA2249: Use 'string.Contains' instead of 'string.IndexOf' to improve readability
|
||||||
|
dotnet_diagnostic.CA2249.severity = error
|
||||||
|
|
||||||
|
# error on CA2254: Template should be a static expression
|
||||||
|
dotnet_diagnostic.CA2254.severity = error
|
||||||
|
|
||||||
|
################
|
||||||
|
### SUGGESTION #
|
||||||
|
################
|
||||||
|
# disable warning CA1014: Mark assemblies with CLSCompliantAttribute
|
||||||
|
dotnet_diagnostic.CA1014.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1024: Use properties where appropriate
|
||||||
|
dotnet_diagnostic.CA1024.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1031: Do not catch general exception types
|
||||||
|
dotnet_diagnostic.CA1031.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1032: Implement standard exception constructors
|
||||||
|
dotnet_diagnostic.CA1032.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1040: Avoid empty interfaces
|
||||||
|
dotnet_diagnostic.CA1040.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1062: Validate arguments of public methods
|
||||||
|
dotnet_diagnostic.CA1062.severity = suggestion
|
||||||
|
|
||||||
|
# TODO: enable when false positives are fixed
|
||||||
|
# disable warning CA1508: Avoid dead conditional code
|
||||||
|
dotnet_diagnostic.CA1508.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1515: Consider making public types internal
|
||||||
|
dotnet_diagnostic.CA1515.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1716: Identifiers should not match keywords
|
||||||
|
dotnet_diagnostic.CA1716.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1720: Identifiers should not contain type names
|
||||||
|
dotnet_diagnostic.CA1720.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1724: Type names should not match namespaces
|
||||||
|
dotnet_diagnostic.CA1724.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1805: Do not initialize unnecessarily
|
||||||
|
dotnet_diagnostic.CA1805.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1812: internal class that is apparently never instantiated.
|
||||||
|
# If so, remove the code from the assembly.
|
||||||
|
# If this class is intended to contain only static members, make it static
|
||||||
|
dotnet_diagnostic.CA1812.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA1822: Member does not access instance data and can be marked as static
|
||||||
|
dotnet_diagnostic.CA1822.severity = suggestion
|
||||||
|
|
||||||
|
# CA1859: Use concrete types when possible for improved performance
|
||||||
|
dotnet_diagnostic.CA1859.severity = suggestion
|
||||||
|
|
||||||
|
# TODO: Enable
|
||||||
|
# CA1861: Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array
|
||||||
|
dotnet_diagnostic.CA1861.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA2000: Dispose objects before losing scope
|
||||||
|
dotnet_diagnostic.CA2000.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA2253: Named placeholders should not be numeric values
|
||||||
|
dotnet_diagnostic.CA2253.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning CA5394: Do not use insecure randomness
|
||||||
|
dotnet_diagnostic.CA5394.severity = suggestion
|
||||||
|
|
||||||
|
# error on CA3003: Review code for file path injection vulnerabilities
|
||||||
|
dotnet_diagnostic.CA3003.severity = suggestion
|
||||||
|
|
||||||
|
# error on CA3006: Review code for process command injection vulnerabilities
|
||||||
|
dotnet_diagnostic.CA3006.severity = suggestion
|
||||||
|
|
||||||
|
###############
|
||||||
|
### DISABLED #
|
||||||
|
###############
|
||||||
|
# disable warning SA1009: Closing parenthesis should be followed by a space.
|
||||||
|
dotnet_diagnostic.SA1009.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1011: Closing square bracket should be followed by a space.
|
||||||
|
dotnet_diagnostic.SA1011.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1101: Prefix local calls with 'this.'
|
||||||
|
dotnet_diagnostic.SA1101.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1108: Block statements should not contain embedded comments
|
||||||
|
dotnet_diagnostic.SA1108.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1118: Parameter must not span multiple lines.
|
||||||
|
dotnet_diagnostic.SA1118.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1128:: Put constructor initializers on their own line
|
||||||
|
dotnet_diagnostic.SA1128.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1130: Use lambda syntax
|
||||||
|
dotnet_diagnostic.SA1130.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1200: 'using' directive must appear within a namespace declaration
|
||||||
|
dotnet_diagnostic.SA1200.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1202: 'public' members must come before 'private' members
|
||||||
|
dotnet_diagnostic.SA1202.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1204: Static members must appear before non-static members
|
||||||
|
dotnet_diagnostic.SA1204.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1309: Fields must not begin with an underscore
|
||||||
|
dotnet_diagnostic.SA1309.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1311: Static readonly fields should begin with upper-case letter
|
||||||
|
dotnet_diagnostic.SA1311.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1413: Use trailing comma in multi-line initializers
|
||||||
|
dotnet_diagnostic.SA1413.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1512: Single-line comments must not be followed by blank line
|
||||||
|
dotnet_diagnostic.SA1512.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1515: Single-line comment should be preceded by blank line
|
||||||
|
dotnet_diagnostic.SA1515.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1600: Elements should be documented
|
||||||
|
dotnet_diagnostic.SA1600.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1601: Partial elements should be documented
|
||||||
|
dotnet_diagnostic.SA1601.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1602: Enumeration items should be documented
|
||||||
|
dotnet_diagnostic.SA1602.severity = none
|
||||||
|
|
||||||
|
# disable warning SA1633: The file header is missing or not located at the top of the file
|
||||||
|
dotnet_diagnostic.SA1633.severity = none
|
||||||
|
|
||||||
|
# disable warning CA1054: Change the type of parameter url from string to System.Uri
|
||||||
|
dotnet_diagnostic.CA1054.severity = none
|
||||||
|
|
||||||
|
# disable warning CA1055: URI return values should not be strings
|
||||||
|
dotnet_diagnostic.CA1055.severity = none
|
||||||
|
|
||||||
|
# disable warning CA1056: URI properties should not be strings
|
||||||
|
dotnet_diagnostic.CA1056.severity = none
|
||||||
|
|
||||||
|
# disable warning CA1303: Do not pass literals as localized parameters
|
||||||
|
dotnet_diagnostic.CA1303.severity = none
|
||||||
|
|
||||||
|
# disable warning CA1308: Normalize strings to uppercase
|
||||||
|
dotnet_diagnostic.CA1308.severity = none
|
||||||
|
|
||||||
|
# disable warning CA1848: Use the LoggerMessage delegates
|
||||||
|
dotnet_diagnostic.CA1848.severity = none
|
||||||
|
|
||||||
|
# disable warning CA2101: Specify marshaling for P/Invoke string arguments
|
||||||
|
dotnet_diagnostic.CA2101.severity = none
|
||||||
|
|
||||||
|
# disable warning CA2234: Pass System.Uri objects instead of strings
|
||||||
|
dotnet_diagnostic.CA2234.severity = none
|
||||||
|
|
||||||
|
# error on RS0030: Do not used banned APIs
|
||||||
|
dotnet_diagnostic.RS0030.severity = error
|
||||||
|
|
||||||
|
# disable warning IDISP001: Dispose created
|
||||||
|
dotnet_diagnostic.IDISP001.severity = suggestion
|
||||||
|
|
||||||
|
# TODO: Enable when false positives are fixed
|
||||||
|
# disable warning IDISP003: Dispose previous before re-assigning
|
||||||
|
dotnet_diagnostic.IDISP003.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning IDISP004: Don't ignore created IDisposable
|
||||||
|
dotnet_diagnostic.IDISP004.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning IDISP007: Don't dispose injected
|
||||||
|
dotnet_diagnostic.IDISP007.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning IDISP008: Don't assign member with injected and created disposables
|
||||||
|
dotnet_diagnostic.IDISP008.severity = suggestion
|
||||||
|
|
||||||
|
[tests/**.{cs,vb}]
|
||||||
|
# disable warning SA0001: XML comment analysis is disabled due to project configuration
|
||||||
|
dotnet_diagnostic.SA0001.severity = none
|
||||||
|
|
||||||
|
# disable warning CA1707: Identifiers should not contain underscores
|
||||||
|
dotnet_diagnostic.CA1707.severity = none
|
||||||
|
|
||||||
|
# disable warning CA2007: Consider calling ConfigureAwait on the awaited task
|
||||||
|
dotnet_diagnostic.CA2007.severity = none
|
||||||
|
|
||||||
|
# disable warning CA2234: Pass system uri objects instead of strings
|
||||||
|
dotnet_diagnostic.CA2234.severity = suggestion
|
||||||
|
|
||||||
|
# disable warning xUnit1028: Test methods must have a supported return type.
|
||||||
|
dotnet_diagnostic.xUnit1028.severity = none
|
||||||
|
|
||||||
|
# CA1826: Do not use Enumerable methods on indexable collections
|
||||||
|
dotnet_diagnostic.CA1826.severity = suggestion
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
name: Issue Report
|
name: Issue Report
|
||||||
description: File an issue report
|
description: File an issue report
|
||||||
labels: [bug, triage]
|
labels: [bug, triage]
|
||||||
|
type: Bug
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
id: introduction
|
id: introduction
|
||||||
@@ -14,7 +15,7 @@ body:
|
|||||||
label: "This issue respects the following points:"
|
label: "This issue respects the following points:"
|
||||||
description: All conditions are **required**. Failure to comply with any of these conditions may cause your issue to be closed without comment.
|
description: All conditions are **required**. Failure to comply with any of these conditions may cause your issue to be closed without comment.
|
||||||
options:
|
options:
|
||||||
- label: This is a **bug**, not a question or a configuration issue; Please visit our forum or chat rooms first to troubleshoot with volunteers, before creating a report. The links can be found [here](https://jellyfin.org/contact/).
|
- label: This is a **bug**, not a question or a configuration issue; Please visit our [forum or chat rooms](https://jellyfin.org/contact/) first to troubleshoot with volunteers, before creating a report.
|
||||||
required: true
|
required: true
|
||||||
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_.
|
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_.
|
||||||
required: true
|
required: true
|
||||||
@@ -86,7 +87,7 @@ body:
|
|||||||
label: Jellyfin Server version
|
label: Jellyfin Server version
|
||||||
description: What version of Jellyfin are you using?
|
description: What version of Jellyfin are you using?
|
||||||
options:
|
options:
|
||||||
- 10.9.11+
|
- 10.10.0+
|
||||||
- Master
|
- Master
|
||||||
- Unstable
|
- Unstable
|
||||||
- Older*
|
- Older*
|
||||||
@@ -140,7 +141,9 @@ body:
|
|||||||
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
||||||
- **Base URL**: [e.g. none, yes: /example]
|
- **Base URL**: [e.g. none, yes: /example]
|
||||||
- **Networking**: [e.g. Host, Bridge/NAT]
|
- **Networking**: [e.g. Host, Bridge/NAT]
|
||||||
- **Storage**: [e.g. local, NFS, cloud]
|
- **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD]
|
||||||
|
- **Media Storage**: [e.g. Local HDD, SMB Share]
|
||||||
|
- **External Integrations**: [e.g. Jellystat, Jellyseerr]
|
||||||
value: |
|
value: |
|
||||||
- OS:
|
- OS:
|
||||||
- Linux Kernel:
|
- Linux Kernel:
|
||||||
@@ -155,7 +158,9 @@ body:
|
|||||||
- Reverse Proxy:
|
- Reverse Proxy:
|
||||||
- Base URL:
|
- Base URL:
|
||||||
- Networking:
|
- Networking:
|
||||||
- Storage:
|
- Jellyfin Data Storage:
|
||||||
|
- Media Storage:
|
||||||
|
- External Integrations:
|
||||||
render: markdown
|
render: markdown
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-latest
|
# Disabled in fork — upstream CodeQL requires specific GitHub org permissions and .NET 10 support
|
||||||
|
if: false
|
||||||
|
runs-on: [self-hosted, k3s, linux, amd64]
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -20,18 +22,18 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: ABI Compatibility
|
name: ABI Compatibility
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
@@ -11,17 +11,22 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||||
|
with:
|
||||||
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
dotnet build Jellyfin.Server -o ./out
|
dotnet build Jellyfin.Server -o ./out
|
||||||
|
|
||||||
- name: Upload Head
|
- name: Upload Head
|
||||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: abi-head
|
name: abi-head
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@@ -35,12 +40,17 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||||
|
with:
|
||||||
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
- name: Checkout common ancestor
|
- name: Checkout common ancestor
|
||||||
env:
|
env:
|
||||||
HEAD_REF: ${{ github.head_ref }}
|
HEAD_REF: ${{ github.head_ref }}
|
||||||
@@ -55,7 +65,7 @@ jobs:
|
|||||||
dotnet build Jellyfin.Server -o ./out
|
dotnet build Jellyfin.Server -o ./out
|
||||||
|
|
||||||
- name: Upload Head
|
- name: Upload Head
|
||||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: abi-base
|
name: abi-base
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@@ -67,7 +77,7 @@ jobs:
|
|||||||
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
||||||
|
|
||||||
name: ABI - Difference
|
name: ABI - Difference
|
||||||
if: ${{ github.event_name == 'pull_request_target' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- abi-head
|
- abi-head
|
||||||
@@ -75,13 +85,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download abi-head
|
- name: Download abi-head
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: abi-head
|
name: abi-head
|
||||||
path: abi-head
|
path: abi-head
|
||||||
|
|
||||||
- name: Download abi-base
|
- name: Download abi-base
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: abi-base
|
name: abi-base
|
||||||
path: abi-base
|
path: abi-base
|
||||||
@@ -95,7 +105,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
{
|
{
|
||||||
echo 'body<<EOF'
|
echo 'body<<EOF'
|
||||||
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll; do
|
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 )"
|
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
|
||||||
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
|
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
|
||||||
printf "\n${file}\n${COMPAT_OUTPUT}\n"
|
printf "\n${file}\n${COMPAT_OUTPUT}\n"
|
||||||
@@ -105,7 +115,7 @@ jobs:
|
|||||||
} >> $GITHUB_OUTPUT
|
} >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Find difference comment
|
- name: Find difference comment
|
||||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||||
id: find-comment
|
id: find-comment
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
@@ -113,7 +123,7 @@ jobs:
|
|||||||
body-includes: abi-diff-workflow-comment
|
body-includes: abi-diff-workflow-comment
|
||||||
|
|
||||||
- name: Reply or edit difference comment (changed)
|
- name: Reply or edit difference comment (changed)
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||||
if: ${{ steps.diff.outputs.body != '' }}
|
if: ${{ steps.diff.outputs.body != '' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
@@ -132,7 +142,7 @@ jobs:
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
- name: Reply or edit difference comment (unchanged)
|
- name: Reply or edit difference comment (unchanged)
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||||
if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
- master
|
- master
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request_target:
|
pull_request:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
@@ -16,23 +16,23 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '9.0.x'
|
||||||
- name: Generate openapi.json
|
- name: Generate openapi.json
|
||||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||||
- name: Upload openapi.json
|
- name: Upload openapi.json
|
||||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-head
|
name: openapi-head
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
|
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
|
||||||
|
|
||||||
openapi-base:
|
openapi-base:
|
||||||
name: OpenAPI - BASE
|
name: OpenAPI - BASE
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@@ -55,37 +55,37 @@ jobs:
|
|||||||
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
|
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
|
||||||
git checkout --progress --force $ANCESTOR_REF
|
git checkout --progress --force $ANCESTOR_REF
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '9.0.x'
|
||||||
- name: Generate openapi.json
|
- name: Generate openapi.json
|
||||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||||
- name: Upload openapi.json
|
- name: Upload openapi.json
|
||||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-base
|
name: openapi-base
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
|
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
|
||||||
|
|
||||||
openapi-diff:
|
openapi-diff:
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
||||||
|
|
||||||
name: OpenAPI - Difference
|
name: OpenAPI - Difference
|
||||||
if: ${{ github.event_name == 'pull_request_target' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- openapi-head
|
- openapi-head
|
||||||
- openapi-base
|
- openapi-base
|
||||||
steps:
|
steps:
|
||||||
- name: Download openapi-head
|
- name: Download openapi-head
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-head
|
name: openapi-head
|
||||||
path: openapi-head
|
path: openapi-head
|
||||||
- name: Download openapi-base
|
- name: Download openapi-base
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-base
|
name: openapi-base
|
||||||
path: openapi-base
|
path: openapi-base
|
||||||
@@ -120,14 +120,14 @@ jobs:
|
|||||||
echo "" >> openapi-changes-reply.md
|
echo "" >> openapi-changes-reply.md
|
||||||
echo "</details>" >> openapi-changes-reply.md
|
echo "</details>" >> openapi-changes-reply.md
|
||||||
- name: Find difference comment
|
- name: Find difference comment
|
||||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||||
id: find-comment
|
id: find-comment
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
direction: last
|
direction: last
|
||||||
body-includes: openapi-diff-workflow-comment
|
body-includes: openapi-diff-workflow-comment
|
||||||
- name: Reply or edit difference comment (changed)
|
- name: Reply or edit difference comment (changed)
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||||
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
|
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
@@ -135,7 +135,7 @@ jobs:
|
|||||||
edit-mode: replace
|
edit-mode: replace
|
||||||
body-path: openapi-changes-reply.md
|
body-path: openapi-changes-reply.md
|
||||||
- name: Edit difference comment (unchanged)
|
- name: Edit difference comment (unchanged)
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||||
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
|
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
@@ -148,7 +148,7 @@ jobs:
|
|||||||
|
|
||||||
publish-unstable:
|
publish-unstable:
|
||||||
name: OpenAPI - Publish Unstable Spec
|
name: OpenAPI - Publish Unstable Spec
|
||||||
if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- openapi-head
|
- openapi-head
|
||||||
@@ -158,12 +158,12 @@ jobs:
|
|||||||
run: |-
|
run: |-
|
||||||
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||||
- name: Download openapi-head
|
- name: Download openapi-head
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-head
|
name: openapi-head
|
||||||
path: openapi-head
|
path: openapi-head
|
||||||
- name: Upload openapi.json (unstable) to repository server
|
- name: Upload openapi.json (unstable) to repository server
|
||||||
uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
|
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
|
||||||
with:
|
with:
|
||||||
host: "${{ secrets.REPO_HOST }}"
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
username: "${{ secrets.REPO_USER }}"
|
username: "${{ secrets.REPO_USER }}"
|
||||||
@@ -172,7 +172,7 @@ jobs:
|
|||||||
strip_components: 1
|
strip_components: 1
|
||||||
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||||
- name: Move openapi.json (unstable) into place
|
- name: Move openapi.json (unstable) into place
|
||||||
uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
|
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
|
||||||
with:
|
with:
|
||||||
host: "${{ secrets.REPO_HOST }}"
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
username: "${{ secrets.REPO_USER }}"
|
username: "${{ secrets.REPO_USER }}"
|
||||||
@@ -220,12 +220,12 @@ jobs:
|
|||||||
run: |-
|
run: |-
|
||||||
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
- name: Download openapi-head
|
- name: Download openapi-head
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: openapi-head
|
name: openapi-head
|
||||||
path: openapi-head
|
path: openapi-head
|
||||||
- name: Upload openapi.json (stable) to repository server
|
- name: Upload openapi.json (stable) to repository server
|
||||||
uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
|
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
|
||||||
with:
|
with:
|
||||||
host: "${{ secrets.REPO_HOST }}"
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
username: "${{ secrets.REPO_USER }}"
|
username: "${{ secrets.REPO_USER }}"
|
||||||
@@ -234,7 +234,7 @@ jobs:
|
|||||||
strip_components: 1
|
strip_components: 1
|
||||||
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||||
- name: Move openapi.json (stable) into place
|
- name: Move openapi.json (stable) into place
|
||||||
uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0
|
uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4
|
||||||
with:
|
with:
|
||||||
host: "${{ secrets.REPO_HOST }}"
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
username: "${{ secrets.REPO_USER }}"
|
username: "${{ secrets.REPO_USER }}"
|
||||||
|
|||||||
@@ -9,32 +9,36 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SDK_VERSION: "8.0.x"
|
SDK_VERSION: "9.0.x"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
run-tests:
|
||||||
strategy:
|
runs-on: [self-hosted, k3s, linux, amd64]
|
||||||
matrix:
|
|
||||||
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
|
|
||||||
|
|
||||||
runs-on: "${{ matrix.os }}"
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
# Install .NET — use GITHUB_ENV (not GITHUB_PATH) to set PATH because
|
||||||
with:
|
# ARC self-hosted runner pods don't pick up GITHUB_PATH between steps.
|
||||||
dotnet-version: ${{ env.SDK_VERSION }}
|
- name: Install .NET SDK
|
||||||
|
run: |
|
||||||
|
DOTNET_INSTALL_DIR="$HOME/.dotnet"
|
||||||
|
mkdir -p "$DOTNET_INSTALL_DIR"
|
||||||
|
curl -fsSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 10.0 --install-dir "$DOTNET_INSTALL_DIR"
|
||||||
|
echo "DOTNET_ROOT=$DOTNET_INSTALL_DIR" >> "$GITHUB_ENV"
|
||||||
|
echo "PATH=$DOTNET_INSTALL_DIR:$PATH" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Run DotNet CLI Tests
|
- name: Run DotNet CLI Tests
|
||||||
run: >
|
run: |
|
||||||
dotnet test Jellyfin.sln
|
export PATH="$HOME/.dotnet:$PATH"
|
||||||
--configuration Release
|
dotnet test Jellyfin.sln \
|
||||||
--collect:"XPlat Code Coverage"
|
--configuration Release \
|
||||||
--settings tests/coverletArgs.runsettings
|
--collect:"XPlat Code Coverage" \
|
||||||
--verbosity minimal
|
--settings tests/coverletArgs.runsettings \
|
||||||
|
--verbosity minimal \
|
||||||
|
--filter "Category!=RequiresDocker&FullyQualifiedName!~Integration"
|
||||||
|
|
||||||
- name: Merge code coverage results
|
- name: Merge code coverage results
|
||||||
uses: danielpalme/ReportGenerator-GitHub-Action@62f9e70ab348d56eee76d446b4db903a85ab0ea8 # v5.3.11
|
uses: danielpalme/ReportGenerator-GitHub-Action@ee0ae774f6d3afedcbd1683c1ab21b83670bdf8e # v5.5.1
|
||||||
with:
|
with:
|
||||||
reports: "**/coverage.cobertura.xml"
|
reports: "**/coverage.cobertura.xml"
|
||||||
targetdir: "merged/"
|
targetdir: "merged/"
|
||||||
@@ -42,3 +46,57 @@ jobs:
|
|||||||
|
|
||||||
# TODO - which action / tool to use to publish code coverage results?
|
# TODO - which action / tool to use to publish code coverage results?
|
||||||
# - name: Publish code coverage results
|
# - name: Publish code coverage results
|
||||||
|
|
||||||
|
# Phase 5 transcode coverage gate — runs in parallel with run-tests.
|
||||||
|
# Explicitly targets the three test assemblies most affected by Phase 5 HLS
|
||||||
|
# session-sharing and PostgreSQL media-encoding changes so failures surface
|
||||||
|
# with a dedicated check status independent of the full test matrix.
|
||||||
|
run-phase5-tests:
|
||||||
|
runs-on: [self-hosted, k3s, linux, amd64]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Install .NET SDK
|
||||||
|
run: |
|
||||||
|
DOTNET_INSTALL_DIR="$HOME/.dotnet"
|
||||||
|
mkdir -p "$DOTNET_INSTALL_DIR"
|
||||||
|
curl -fsSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 10.0 --install-dir "$DOTNET_INSTALL_DIR"
|
||||||
|
echo "DOTNET_ROOT=$DOTNET_INSTALL_DIR" >> "$GITHUB_ENV"
|
||||||
|
echo "PATH=$DOTNET_INSTALL_DIR:$PATH" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Run Phase 5 Transcode Tests (API)
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.dotnet:$PATH"
|
||||||
|
dotnet test tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj \
|
||||||
|
--configuration Release \
|
||||||
|
--collect:"XPlat Code Coverage" \
|
||||||
|
--settings tests/coverletArgs.runsettings \
|
||||||
|
--verbosity minimal \
|
||||||
|
--filter "Category!=RequiresDocker"
|
||||||
|
|
||||||
|
- name: Run Phase 5 Transcode Tests (HLS)
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.dotnet:$PATH"
|
||||||
|
dotnet test tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj \
|
||||||
|
--configuration Release \
|
||||||
|
--collect:"XPlat Code Coverage" \
|
||||||
|
--settings tests/coverletArgs.runsettings \
|
||||||
|
--verbosity minimal \
|
||||||
|
--filter "Category!=RequiresDocker"
|
||||||
|
|
||||||
|
- name: Run Phase 5 Transcode Tests (Server.Implementations)
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.dotnet:$PATH"
|
||||||
|
dotnet test tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj \
|
||||||
|
--configuration Release \
|
||||||
|
--collect:"XPlat Code Coverage" \
|
||||||
|
--settings tests/coverletArgs.runsettings \
|
||||||
|
--verbosity minimal \
|
||||||
|
--filter "Category!=RequiresDocker"
|
||||||
|
|
||||||
|
- name: Merge Phase 5 code coverage results
|
||||||
|
uses: danielpalme/ReportGenerator-GitHub-Action@2a7030e9775aab6c78e80cb66843051acdacee3e # v5.5.2
|
||||||
|
with:
|
||||||
|
reports: "**/coverage.cobertura.xml"
|
||||||
|
targetdir: "merged-phase5/"
|
||||||
|
reporttypes: "Cobertura"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
types:
|
types:
|
||||||
- created
|
- created
|
||||||
- edited
|
- edited
|
||||||
pull_request_target:
|
pull_request:
|
||||||
types:
|
types:
|
||||||
- labeled
|
- labeled
|
||||||
- synchronize
|
- synchronize
|
||||||
@@ -17,14 +17,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Notify as seen
|
- name: Notify as seen
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
comment-id: ${{ github.event.comment.id }}
|
comment-id: ${{ github.event.comment.id }}
|
||||||
reactions: '+1'
|
reactions: '+1'
|
||||||
|
|
||||||
- name: Checkout the latest code
|
- name: Checkout the latest code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -34,107 +34,19 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
check-backport:
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
name: Check Backport
|
|
||||||
if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Notify as seen
|
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
|
||||||
if: ${{ github.event.comment != null }}
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
comment-id: ${{ github.event.comment.id }}
|
|
||||||
reactions: eyes
|
|
||||||
|
|
||||||
- name: Checkout the latest code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Notify as running
|
|
||||||
id: comment_running
|
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
|
||||||
if: ${{ github.event.comment != null }}
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
issue-number: ${{ github.event.issue.number }}
|
|
||||||
body: |
|
|
||||||
Running backport tests...
|
|
||||||
|
|
||||||
- name: Perform test backport
|
|
||||||
id: run_tests
|
|
||||||
run: |
|
|
||||||
set +o errexit
|
|
||||||
git config --global user.name "Jellyfin Bot"
|
|
||||||
git config --global user.email "team@jellyfin.org"
|
|
||||||
CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
|
|
||||||
git checkout master
|
|
||||||
git merge --no-ff ${CURRENT_BRANCH}
|
|
||||||
MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
|
|
||||||
git fetch --all
|
|
||||||
CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
|
|
||||||
stable_branch="Current stable release branch: ${CURRENT_STABLE}"
|
|
||||||
echo ${stable_branch}
|
|
||||||
echo ::set-output name=branch::${stable_branch}
|
|
||||||
git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
|
|
||||||
git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
|
|
||||||
retcode=$?
|
|
||||||
cat output.txt | grep -v 'hint:'
|
|
||||||
output="$( grep -v 'hint:' output.txt )"
|
|
||||||
output="${output//'%'/'%25'}"
|
|
||||||
output="${output//$'\n'/'%0A'}"
|
|
||||||
output="${output//$'\r'/'%0D'}"
|
|
||||||
echo ::set-output name=output::$output
|
|
||||||
exit ${retcode}
|
|
||||||
|
|
||||||
- name: Notify with result success
|
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
|
||||||
if: ${{ github.event.comment != null && success() }}
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
|
||||||
body: |
|
|
||||||
${{ steps.run_tests.outputs.branch }}
|
|
||||||
Output from `git cherry-pick`:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
${{ steps.run_tests.outputs.output }}
|
|
||||||
reactions: hooray
|
|
||||||
|
|
||||||
- name: Notify with result failure
|
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
|
||||||
if: ${{ github.event.comment != null && failure() }}
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
|
||||||
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
|
||||||
body: |
|
|
||||||
${{ steps.run_tests.outputs.branch }}
|
|
||||||
Output from `git cherry-pick`:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
${{ steps.run_tests.outputs.output }}
|
|
||||||
reactions: confused
|
|
||||||
|
|
||||||
rename:
|
rename:
|
||||||
name: Rename
|
name: Rename
|
||||||
if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'
|
if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: pull in script
|
- name: pull in script
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
repository: jellyfin/jellyfin-triage-script
|
repository: jellyfin/jellyfin-triage-script
|
||||||
- name: install python
|
- name: install python
|
||||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.14'
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
- name: install python packages
|
- name: install python packages
|
||||||
run: pip install -r rename/requirements.txt
|
run: pip install -r rename/requirements.txt
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
name: HA Build & Push to ECR
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- main
|
||||||
|
- "feat/ha-*"
|
||||||
|
- "feat/phase*"
|
||||||
|
- "copilot/*"
|
||||||
|
# pull_request intentionally removed: this workflow runs on self-hosted k3s
|
||||||
|
# runners. Allowing pull_request events from a public repo would let any
|
||||||
|
# internet user execute arbitrary code inside the cluster network.
|
||||||
|
# CI build feedback on PRs is provided by ci-tests.yml (GitHub-hosted runners).
|
||||||
|
|
||||||
|
# Cancel in-progress runs when a new push arrives on the same branch.
|
||||||
|
concurrency:
|
||||||
|
group: ha-build-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: [self-hosted, k3s, linux, amd64]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
||||||
|
with:
|
||||||
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Login to Amazon ECR
|
||||||
|
id: ecr-login
|
||||||
|
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1
|
||||||
|
|
||||||
|
- name: Set image metadata
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
REPO="${{ steps.ecr-login.outputs.registry }}/${{ secrets.ECR_REPOSITORY }}"
|
||||||
|
SHORT_SHA="${GITHUB_SHA::7}"
|
||||||
|
echo "image_repo=${REPO}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Install .NET SDK
|
||||||
|
# Build on the runner host filesystem (native I/O) to avoid DinD
|
||||||
|
# overlay-on-overlay throttling which makes dotnet publish ~20x slower.
|
||||||
|
run: |
|
||||||
|
DOTNET_INSTALL_DIR="$HOME/.dotnet"
|
||||||
|
mkdir -p "$DOTNET_INSTALL_DIR"
|
||||||
|
if ! "$DOTNET_INSTALL_DIR/dotnet" --version 2>/dev/null | grep -q "^10\\."; then
|
||||||
|
curl -fsSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 10.0 --install-dir "$DOTNET_INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
echo "DOTNET_ROOT=$DOTNET_INSTALL_DIR" >> "$GITHUB_ENV"
|
||||||
|
echo "PATH=$DOTNET_INSTALL_DIR:$PATH" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Restore NuGet packages
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.dotnet:$PATH"
|
||||||
|
dotnet restore Jellyfin.Server/Jellyfin.Server.csproj --runtime linux-x64
|
||||||
|
|
||||||
|
- name: Publish Jellyfin server
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.dotnet:$PATH"
|
||||||
|
dotnet publish Jellyfin.Server/Jellyfin.Server.csproj \
|
||||||
|
--configuration Release \
|
||||||
|
--runtime linux-x64 \
|
||||||
|
--self-contained false \
|
||||||
|
--no-restore \
|
||||||
|
-p:TreatWarningsAsErrors=false \
|
||||||
|
--output ./publish-output
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.runtime
|
||||||
|
platforms: linux/amd64
|
||||||
|
# Only push the image on direct branch pushes, not on pull_request events.
|
||||||
|
push: ${{ github.event_name == 'push' }}
|
||||||
|
provenance: false
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.image_repo }}:${{ steps.meta.outputs.short_sha }}
|
||||||
|
${{ steps.meta.outputs.image_repo }}:latest
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
ascending: true
|
ascending: true
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: pull in script
|
- name: pull in script
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
repository: jellyfin/jellyfin-triage-script
|
repository: jellyfin/jellyfin-triage-script
|
||||||
- name: install python
|
- name: install python
|
||||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.14'
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
- name: install python packages
|
- name: install python packages
|
||||||
run: pip install -r main-repo-triage/requirements.txt
|
run: pip install -r main-repo-triage/requirements.txt
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request_target:
|
pull_request:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request_target:
|
pull_request:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
@@ -12,11 +12,11 @@ jobs:
|
|||||||
label:
|
label:
|
||||||
name: Labeling
|
name: Labeling
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
if: ${{ github.repository == 'jellyfin/jellyfin' && github.event.issue.pull_request }}
|
||||||
steps:
|
steps:
|
||||||
- name: Apply label
|
- name: Apply label
|
||||||
uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
|
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request'}}
|
||||||
with:
|
with:
|
||||||
dirtyLabel: 'merge conflict'
|
dirtyLabel: 'merge conflict'
|
||||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
ascending: true
|
ascending: true
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
yq-version: v4.9.8
|
yq-version: v4.9.8
|
||||||
|
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.TAG_BRANCH }}
|
ref: ${{ env.TAG_BRANCH }}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.TAG_BRANCH }}
|
ref: ${{ env.TAG_BRANCH }}
|
||||||
|
|
||||||
|
|||||||
Vendored
+6
-5
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"ms-dotnettools.csharp",
|
"ms-dotnettools.csharp",
|
||||||
"editorconfig.editorconfig",
|
"editorconfig.editorconfig",
|
||||||
"github.vscode-github-actions",
|
"github.vscode-github-actions",
|
||||||
"ms-dotnettools.vscode-dotnet-runtime",
|
"ms-dotnettools.vscode-dotnet-runtime",
|
||||||
"ms-dotnettools.csdevkit"
|
"ms-dotnettools.csdevkit",
|
||||||
],
|
"alexcvzz.vscode-sqlite"
|
||||||
"unwantedRecommendations": [
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+3
-3
@@ -6,7 +6,7 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
|
||||||
"args": ["--nowebclient"],
|
"args": ["--nowebclient"],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"type": "coreclr",
|
"type": "coreclr",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
|
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
|
||||||
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
|
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
|
||||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
|
|||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"dotnet.preferVisualStudioCodeFileSystemWatcher": true
|
||||||
|
}
|
||||||
@@ -27,9 +27,11 @@
|
|||||||
- [cryptobank](https://github.com/cryptobank)
|
- [cryptobank](https://github.com/cryptobank)
|
||||||
- [cvium](https://github.com/cvium)
|
- [cvium](https://github.com/cvium)
|
||||||
- [dannymichel](https://github.com/dannymichel)
|
- [dannymichel](https://github.com/dannymichel)
|
||||||
|
- [darioackermann](https://github.com/darioackermann)
|
||||||
- [DaveChild](https://github.com/DaveChild)
|
- [DaveChild](https://github.com/DaveChild)
|
||||||
- [DavidFair](https://github.com/DavidFair)
|
- [DavidFair](https://github.com/DavidFair)
|
||||||
- [Delgan](https://github.com/Delgan)
|
- [Delgan](https://github.com/Delgan)
|
||||||
|
- [Derpipose](https://github.com/Derpipose)
|
||||||
- [dcrdev](https://github.com/dcrdev)
|
- [dcrdev](https://github.com/dcrdev)
|
||||||
- [dhartung](https://github.com/dhartung)
|
- [dhartung](https://github.com/dhartung)
|
||||||
- [dinki](https://github.com/dinki)
|
- [dinki](https://github.com/dinki)
|
||||||
@@ -60,6 +62,7 @@
|
|||||||
- [ikomhoog](https://github.com/ikomhoog)
|
- [ikomhoog](https://github.com/ikomhoog)
|
||||||
- [iwalton3](https://github.com/iwalton3)
|
- [iwalton3](https://github.com/iwalton3)
|
||||||
- [jftuga](https://github.com/jftuga)
|
- [jftuga](https://github.com/jftuga)
|
||||||
|
- [jkhsjdhjs](https://github.com/jkhsjdhjs)
|
||||||
- [jmshrv](https://github.com/jmshrv)
|
- [jmshrv](https://github.com/jmshrv)
|
||||||
- [joern-h](https://github.com/joern-h)
|
- [joern-h](https://github.com/joern-h)
|
||||||
- [joshuaboniface](https://github.com/joshuaboniface)
|
- [joshuaboniface](https://github.com/joshuaboniface)
|
||||||
@@ -114,6 +117,7 @@
|
|||||||
- [sachk](https://github.com/sachk)
|
- [sachk](https://github.com/sachk)
|
||||||
- [sammyrc34](https://github.com/sammyrc34)
|
- [sammyrc34](https://github.com/sammyrc34)
|
||||||
- [samuel9554](https://github.com/samuel9554)
|
- [samuel9554](https://github.com/samuel9554)
|
||||||
|
- [SapientGuardian](https://github.com/SapientGuardian)
|
||||||
- [scheidleon](https://github.com/scheidleon)
|
- [scheidleon](https://github.com/scheidleon)
|
||||||
- [sebPomme](https://github.com/sebPomme)
|
- [sebPomme](https://github.com/sebPomme)
|
||||||
- [SegiH](https://github.com/SegiH)
|
- [SegiH](https://github.com/SegiH)
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
- [ThibaultNocchi](https://github.com/ThibaultNocchi)
|
- [ThibaultNocchi](https://github.com/ThibaultNocchi)
|
||||||
- [thornbill](https://github.com/thornbill)
|
- [thornbill](https://github.com/thornbill)
|
||||||
- [ThreeFive-O](https://github.com/ThreeFive-O)
|
- [ThreeFive-O](https://github.com/ThreeFive-O)
|
||||||
|
- [tjwalkr3](https://github.com/tjwalkr3)
|
||||||
- [TrisMcC](https://github.com/TrisMcC)
|
- [TrisMcC](https://github.com/TrisMcC)
|
||||||
- [trumblejoe](https://github.com/trumblejoe)
|
- [trumblejoe](https://github.com/trumblejoe)
|
||||||
- [TtheCreator](https://github.com/TtheCreator)
|
- [TtheCreator](https://github.com/TtheCreator)
|
||||||
@@ -192,6 +197,17 @@
|
|||||||
- [jaina heartles](https://github.com/heartles)
|
- [jaina heartles](https://github.com/heartles)
|
||||||
- [oxixes](https://github.com/oxixes)
|
- [oxixes](https://github.com/oxixes)
|
||||||
- [elfalem](https://github.com/elfalem)
|
- [elfalem](https://github.com/elfalem)
|
||||||
|
- [Kenneth Cochran](https://github.com/kennethcochran)
|
||||||
|
- [benedikt257](https://github.com/benedikt257)
|
||||||
|
- [revam](https://github.com/revam)
|
||||||
|
- [allesmi](https://github.com/allesmi)
|
||||||
|
- [ThunderClapLP](https://github.com/ThunderClapLP)
|
||||||
|
- [Shoham Peller](https://github.com/spellr)
|
||||||
|
- [theshoeshiner](https://github.com/theshoeshiner)
|
||||||
|
- [TokerX](https://github.com/TokerX)
|
||||||
|
- [GeneMarks](https://github.com/GeneMarks)
|
||||||
|
- [martenumberto](https://github.com/martenumberto)
|
||||||
|
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
@@ -265,3 +281,6 @@
|
|||||||
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
||||||
- [Robert Lützner](https://github.com/rluetzner)
|
- [Robert Lützner](https://github.com/rluetzner)
|
||||||
- [Nathan McCrina](https://github.com/nfmccrina)
|
- [Nathan McCrina](https://github.com/nfmccrina)
|
||||||
|
- [Martin Reuter](https://github.com/reuterma24)
|
||||||
|
- [Michael McElroy](https://github.com/mcmcelro)
|
||||||
|
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<WarningsNotAsErrors>NU1902;NU1903</WarningsNotAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
@@ -19,4 +19,9 @@
|
|||||||
<AdditionalFiles Include="$(MSBuildThisFileDirectory)/stylecop.json" />
|
<AdditionalFiles Include="$(MSBuildThisFileDirectory)/stylecop.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Custom Analyzers -->
|
||||||
|
<ItemGroup Condition=" '$(MSBuildProjectName)' != 'Jellyfin.CodeAnalysis' AND '$(Configuration)' == 'Debug' ">
|
||||||
|
<ProjectReference Include="$(MSBuildThisFileDirectory)src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj" OutputItemType="Analyzer" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+69
-56
@@ -4,88 +4,101 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||||
<ItemGroup Label="Package Dependencies">
|
<ItemGroup Label="Package Dependencies">
|
||||||
<PackageVersion Include="AsyncKeyedLock" Version="7.0.2" />
|
<PackageVersion Include="AsyncKeyedLock" Version="7.1.8" />
|
||||||
|
<PackageVersion Include="AWSSDK.S3" Version="3.7.410.2" />
|
||||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||||
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
||||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.3" />
|
<PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
|
||||||
<PackageVersion Include="BlurHashSharp" Version="1.3.3" />
|
<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" />
|
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
<PackageVersion Include="Diacritics" Version="4.0.17" />
|
||||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
<PackageVersion Include="FsCheck.Xunit" Version="3.3.2" />
|
||||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
|
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
|
||||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||||
|
<PackageVersion Include="Ignore" Version="0.2.1" />
|
||||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||||
<PackageVersion Include="libse" Version="4.0.8" />
|
<PackageVersion Include="libse" Version="4.0.12" />
|
||||||
<PackageVersion Include="LrcParser" Version="2024.0728.2" />
|
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
||||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.10" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.11" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" />
|
||||||
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.11" />
|
||||||
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.11" />
|
||||||
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||||
|
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||||
|
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
|
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
||||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||||
<PackageVersion Include="NEbml" Version="0.11.0" />
|
<PackageVersion Include="NEbml" Version="1.1.0.5" />
|
||||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
|
<PackageVersion Include="Npgsql" Version="9.0.4" />
|
||||||
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
|
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
|
||||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
|
<PackageVersion Include="Polly" Version="8.6.5" />
|
||||||
|
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||||
|
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||||
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
|
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
|
||||||
<PackageVersion Include="SkiaSharp" Version="2.88.8" />
|
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
|
||||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.8" />
|
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
|
||||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
|
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
|
||||||
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
|
||||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||||
<PackageVersion Include="Svg.Skia" Version="2.0.0.1" />
|
<PackageVersion Include="Svg.Skia" Version="3.2.1" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" />
|
||||||
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
|
<PackageVersion Include="System.Text.Json" Version="9.0.11" />
|
||||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.11" />
|
||||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||||
<PackageVersion Include="z440.atl.core" Version="6.6.0" />
|
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.4.0" />
|
||||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
<PackageVersion Include="z440.atl.core" Version="7.9.0" />
|
||||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
<PackageVersion Include="TMDbLib" Version="2.3.0" />
|
||||||
|
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||||
|
<PackageVersion Include="StackExchange.Redis" Version="2.8.16" />
|
||||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
||||||
<PackageVersion Include="xunit" Version="2.9.2" />
|
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+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. 10.11.6+deb12).
|
||||||
|
# Web assets land at /usr/share/jellyfin/web/ — stable, prebuilt, no npm required.
|
||||||
|
# 10.11.6 is the latest stable web client; API-compatible with the 10.12.0 server.
|
||||||
|
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=10.11.6+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. 10.11.6+deb12).
|
||||||
|
# Web assets land at /usr/share/jellyfin/web/ — stable, prebuilt, no npm required.
|
||||||
|
# 10.11.6 is the latest stable web client; API-compatible with the 10.12.0 server.
|
||||||
|
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=10.11.6+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"]
|
||||||
@@ -21,8 +21,8 @@ namespace Emby.Naming.Common
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public NamingOptions()
|
public NamingOptions()
|
||||||
{
|
{
|
||||||
VideoFileExtensions = new[]
|
VideoFileExtensions =
|
||||||
{
|
[
|
||||||
".001",
|
".001",
|
||||||
".3g2",
|
".3g2",
|
||||||
".3gp",
|
".3gp",
|
||||||
@@ -77,10 +77,10 @@ namespace Emby.Naming.Common
|
|||||||
".wmv",
|
".wmv",
|
||||||
".wtv",
|
".wtv",
|
||||||
".xvid"
|
".xvid"
|
||||||
};
|
];
|
||||||
|
|
||||||
VideoFlagDelimiters = new[]
|
VideoFlagDelimiters =
|
||||||
{
|
[
|
||||||
'(',
|
'(',
|
||||||
')',
|
')',
|
||||||
'-',
|
'-',
|
||||||
@@ -88,15 +88,15 @@ namespace Emby.Naming.Common
|
|||||||
'_',
|
'_',
|
||||||
'[',
|
'[',
|
||||||
']'
|
']'
|
||||||
};
|
];
|
||||||
|
|
||||||
StubFileExtensions = new[]
|
StubFileExtensions =
|
||||||
{
|
[
|
||||||
".disc"
|
".disc"
|
||||||
};
|
];
|
||||||
|
|
||||||
StubTypes = new[]
|
StubTypes =
|
||||||
{
|
[
|
||||||
new StubTypeRule(
|
new StubTypeRule(
|
||||||
stubType: "dvd",
|
stubType: "dvd",
|
||||||
token: "dvd"),
|
token: "dvd"),
|
||||||
@@ -136,32 +136,32 @@ namespace Emby.Naming.Common
|
|||||||
new StubTypeRule(
|
new StubTypeRule(
|
||||||
stubType: "tv",
|
stubType: "tv",
|
||||||
token: "DSR")
|
token: "DSR")
|
||||||
};
|
];
|
||||||
|
|
||||||
VideoFileStackingRules = new[]
|
VideoFileStackingRules =
|
||||||
{
|
[
|
||||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
|
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
|
||||||
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
|
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
|
||||||
};
|
];
|
||||||
|
|
||||||
CleanDateTimes = new[]
|
CleanDateTimes =
|
||||||
{
|
[
|
||||||
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
|
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
|
||||||
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
|
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
|
||||||
};
|
];
|
||||||
|
|
||||||
CleanStrings = new[]
|
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|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|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
||||||
@"^(?<cleaned>.+?)(\[.*\])",
|
@"^(?<cleaned>.+?)(\[.*\])",
|
||||||
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
|
||||||
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
|
||||||
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
|
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
|
||||||
@"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
|
@"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
|
||||||
};
|
];
|
||||||
|
|
||||||
SubtitleFileExtensions = new[]
|
SubtitleFileExtensions =
|
||||||
{
|
[
|
||||||
".ass",
|
".ass",
|
||||||
".mks",
|
".mks",
|
||||||
".sami",
|
".sami",
|
||||||
@@ -171,27 +171,29 @@ namespace Emby.Naming.Common
|
|||||||
".sub",
|
".sub",
|
||||||
".sup",
|
".sup",
|
||||||
".vtt",
|
".vtt",
|
||||||
};
|
];
|
||||||
|
|
||||||
LyricFileExtensions = new[]
|
LyricFileExtensions =
|
||||||
{
|
[
|
||||||
".lrc",
|
".lrc",
|
||||||
".elrc",
|
".elrc",
|
||||||
".txt"
|
".txt"
|
||||||
};
|
];
|
||||||
|
|
||||||
AlbumStackingPrefixes = new[]
|
AlbumStackingPrefixes =
|
||||||
{
|
[
|
||||||
"cd",
|
"cd",
|
||||||
"digital media",
|
"digital media",
|
||||||
"disc",
|
"disc",
|
||||||
"disk",
|
"disk",
|
||||||
"vol",
|
"vol",
|
||||||
"volume"
|
"volume",
|
||||||
};
|
"part",
|
||||||
|
"act"
|
||||||
|
];
|
||||||
|
|
||||||
ArtistSubfolders = new[]
|
ArtistSubfolders =
|
||||||
{
|
[
|
||||||
"albums",
|
"albums",
|
||||||
"broadcasts",
|
"broadcasts",
|
||||||
"bootlegs",
|
"bootlegs",
|
||||||
@@ -206,10 +208,10 @@ namespace Emby.Naming.Common
|
|||||||
"soundtracks",
|
"soundtracks",
|
||||||
"spokenwords",
|
"spokenwords",
|
||||||
"streets"
|
"streets"
|
||||||
};
|
];
|
||||||
|
|
||||||
AudioFileExtensions = new[]
|
AudioFileExtensions =
|
||||||
{
|
[
|
||||||
".669",
|
".669",
|
||||||
".3gp",
|
".3gp",
|
||||||
".aa",
|
".aa",
|
||||||
@@ -238,6 +240,8 @@ namespace Emby.Naming.Common
|
|||||||
".dsp",
|
".dsp",
|
||||||
".dts",
|
".dts",
|
||||||
".dvf",
|
".dvf",
|
||||||
|
".eac3",
|
||||||
|
".ec3",
|
||||||
".far",
|
".far",
|
||||||
".flac",
|
".flac",
|
||||||
".gdm",
|
".gdm",
|
||||||
@@ -288,33 +292,33 @@ namespace Emby.Naming.Common
|
|||||||
".xm",
|
".xm",
|
||||||
".xsp",
|
".xsp",
|
||||||
".ymf"
|
".ymf"
|
||||||
};
|
];
|
||||||
|
|
||||||
MediaFlagDelimiters = new[]
|
MediaFlagDelimiters =
|
||||||
{
|
[
|
||||||
'.'
|
'.'
|
||||||
};
|
];
|
||||||
|
|
||||||
MediaForcedFlags = new[]
|
MediaForcedFlags =
|
||||||
{
|
[
|
||||||
"foreign",
|
"foreign",
|
||||||
"forced"
|
"forced"
|
||||||
};
|
];
|
||||||
|
|
||||||
MediaDefaultFlags = new[]
|
MediaDefaultFlags =
|
||||||
{
|
[
|
||||||
"default"
|
"default"
|
||||||
};
|
];
|
||||||
|
|
||||||
MediaHearingImpairedFlags = new[]
|
MediaHearingImpairedFlags =
|
||||||
{
|
[
|
||||||
"cc",
|
"cc",
|
||||||
"hi",
|
"hi",
|
||||||
"sdh"
|
"sdh"
|
||||||
};
|
];
|
||||||
|
|
||||||
EpisodeExpressions = new[]
|
EpisodeExpressions =
|
||||||
{
|
[
|
||||||
// *** Begin Kodi Standard Naming
|
// *** Begin Kodi Standard Naming
|
||||||
// <!-- foo.s01.e01, foo.s01_e01, S01E02 foo, S01 - E02 -->
|
// <!-- foo.s01.e01, foo.s01_e01, S01E02 foo, S01 - E02 -->
|
||||||
new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?<seasonnumber>[0-9]+)[][ ._-]*[Ee](?<epnumber>[0-9]+)([^\\/]*)$")
|
new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?<seasonnumber>[0-9]+)[][ ._-]*[Ee](?<epnumber>[0-9]+)([^\\/]*)$")
|
||||||
@@ -327,23 +331,23 @@ namespace Emby.Naming.Common
|
|||||||
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
|
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
|
||||||
new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
|
new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
|
||||||
{
|
{
|
||||||
DateTimeFormats = new[]
|
DateTimeFormats =
|
||||||
{
|
[
|
||||||
"yyyy.MM.dd",
|
"yyyy.MM.dd",
|
||||||
"yyyy-MM-dd",
|
"yyyy-MM-dd",
|
||||||
"yyyy_MM_dd",
|
"yyyy_MM_dd",
|
||||||
"yyyy MM dd"
|
"yyyy MM dd"
|
||||||
}
|
]
|
||||||
},
|
},
|
||||||
new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
|
new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
|
||||||
{
|
{
|
||||||
DateTimeFormats = new[]
|
DateTimeFormats =
|
||||||
{
|
[
|
||||||
"dd.MM.yyyy",
|
"dd.MM.yyyy",
|
||||||
"dd-MM-yyyy",
|
"dd-MM-yyyy",
|
||||||
"dd_MM_yyyy",
|
"dd_MM_yyyy",
|
||||||
"dd MM yyyy"
|
"dd MM yyyy"
|
||||||
}
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
// This isn't a Kodi naming rule, but the expression below causes false episode numbers for
|
// This isn't a Kodi naming rule, but the expression below causes false episode numbers for
|
||||||
@@ -467,10 +471,18 @@ namespace Emby.Naming.Common
|
|||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
|
||||||
VideoExtraRules = new[]
|
// Anime style expression
|
||||||
{
|
// "[Group][Series Name][21][1080p][FLAC][HASH]"
|
||||||
|
// "[Group] Series Name [04][BDRIP]"
|
||||||
|
new EpisodeExpression(@"(?:\[(?:[^\]]+)\]\s*)?(?<seriesname>\[[^\]]+\]|[^[\]]+)\s*\[(?<epnumber>[0-9]+)\]")
|
||||||
|
{
|
||||||
|
IsNamed = true
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
VideoExtraRules =
|
||||||
|
[
|
||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
ExtraType.Trailer,
|
ExtraType.Trailer,
|
||||||
ExtraRuleType.DirectoryName,
|
ExtraRuleType.DirectoryName,
|
||||||
@@ -561,6 +573,18 @@ namespace Emby.Naming.Common
|
|||||||
"trailer",
|
"trailer",
|
||||||
MediaType.Video),
|
MediaType.Video),
|
||||||
|
|
||||||
|
new ExtraRule(
|
||||||
|
ExtraType.Sample,
|
||||||
|
ExtraRuleType.Filename,
|
||||||
|
"sample",
|
||||||
|
MediaType.Video),
|
||||||
|
|
||||||
|
new ExtraRule(
|
||||||
|
ExtraType.ThemeSong,
|
||||||
|
ExtraRuleType.Filename,
|
||||||
|
"theme",
|
||||||
|
MediaType.Audio),
|
||||||
|
|
||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
ExtraType.Trailer,
|
ExtraType.Trailer,
|
||||||
ExtraRuleType.Suffix,
|
ExtraRuleType.Suffix,
|
||||||
@@ -582,13 +606,7 @@ namespace Emby.Naming.Common
|
|||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
ExtraType.Trailer,
|
ExtraType.Trailer,
|
||||||
ExtraRuleType.Suffix,
|
ExtraRuleType.Suffix,
|
||||||
" trailer",
|
"- trailer",
|
||||||
MediaType.Video),
|
|
||||||
|
|
||||||
new ExtraRule(
|
|
||||||
ExtraType.Sample,
|
|
||||||
ExtraRuleType.Filename,
|
|
||||||
"sample",
|
|
||||||
MediaType.Video),
|
MediaType.Video),
|
||||||
|
|
||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
@@ -612,15 +630,9 @@ namespace Emby.Naming.Common
|
|||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
ExtraType.Sample,
|
ExtraType.Sample,
|
||||||
ExtraRuleType.Suffix,
|
ExtraRuleType.Suffix,
|
||||||
" sample",
|
"- sample",
|
||||||
MediaType.Video),
|
MediaType.Video),
|
||||||
|
|
||||||
new ExtraRule(
|
|
||||||
ExtraType.ThemeSong,
|
|
||||||
ExtraRuleType.Filename,
|
|
||||||
"theme",
|
|
||||||
MediaType.Audio),
|
|
||||||
|
|
||||||
new ExtraRule(
|
new ExtraRule(
|
||||||
ExtraType.Scene,
|
ExtraType.Scene,
|
||||||
ExtraRuleType.Suffix,
|
ExtraRuleType.Suffix,
|
||||||
@@ -680,14 +692,14 @@ namespace Emby.Naming.Common
|
|||||||
ExtraRuleType.Suffix,
|
ExtraRuleType.Suffix,
|
||||||
"-other",
|
"-other",
|
||||||
MediaType.Video)
|
MediaType.Video)
|
||||||
};
|
];
|
||||||
|
|
||||||
AllExtrasTypesFolderNames = VideoExtraRules
|
AllExtrasTypesFolderNames = VideoExtraRules
|
||||||
.Where(i => i.RuleType == ExtraRuleType.DirectoryName)
|
.Where(i => i.RuleType == ExtraRuleType.DirectoryName)
|
||||||
.ToDictionary(i => i.Token, i => i.ExtraType, StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(i => i.Token, i => i.ExtraType, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
Format3DRules = new[]
|
Format3DRules =
|
||||||
{
|
[
|
||||||
// Kodi rules:
|
// Kodi rules:
|
||||||
new Format3DRule(
|
new Format3DRule(
|
||||||
precedingToken: "3d",
|
precedingToken: "3d",
|
||||||
@@ -714,10 +726,10 @@ namespace Emby.Naming.Common
|
|||||||
new Format3DRule("tab"),
|
new Format3DRule("tab"),
|
||||||
new Format3DRule("sbs3d"),
|
new Format3DRule("sbs3d"),
|
||||||
new Format3DRule("mvc")
|
new Format3DRule("mvc")
|
||||||
};
|
];
|
||||||
|
|
||||||
AudioBookPartsExpressions = new[]
|
AudioBookPartsExpressions =
|
||||||
{
|
[
|
||||||
// Detect specified chapters, like CH 01
|
// Detect specified chapters, like CH 01
|
||||||
@"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
|
@"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
|
||||||
// Detect specified parts, like Part 02
|
// Detect specified parts, like Part 02
|
||||||
@@ -730,14 +742,14 @@ namespace Emby.Naming.Common
|
|||||||
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
|
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
|
||||||
// Some audiobooks are ripped from cd's, and will be named by disk number.
|
// Some audiobooks are ripped from cd's, and will be named by disk number.
|
||||||
@"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
|
@"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
|
||||||
};
|
];
|
||||||
|
|
||||||
AudioBookNamesExpressions = new[]
|
AudioBookNamesExpressions =
|
||||||
{
|
[
|
||||||
// Detect year usually in brackets after name Batman (2020)
|
// Detect year usually in brackets after name Batman (2020)
|
||||||
@"^(?<name>.+?)\s*\(\s*(?<year>[0-9]{4})\s*\)\s*$",
|
@"^(?<name>.+?)\s*\(\s*(?<year>[0-9]{4})\s*\)\s*$",
|
||||||
@"^\s*(?<name>[^ ].*?)\s*$"
|
@"^\s*(?<name>[^ ].*?)\s*$"
|
||||||
};
|
];
|
||||||
|
|
||||||
MultipleEpisodeExpressions = new[]
|
MultipleEpisodeExpressions = new[]
|
||||||
{
|
{
|
||||||
@@ -877,12 +889,12 @@ namespace Emby.Naming.Common
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets list of clean datetime regular expressions.
|
/// Gets list of clean datetime regular expressions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty<Regex>();
|
public Regex[] CleanDateTimeRegexes { get; private set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets list of clean string regular expressions.
|
/// Gets list of clean string regular expressions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
|
public Regex[] CleanStringRegexes { get; private set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compiles raw regex strings into regexes.
|
/// Compiles raw regex strings into regexes.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.11.7</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -97,14 +97,18 @@ namespace Emby.Naming.ExternalFiles
|
|||||||
|
|
||||||
if (culture is not null && pathInfo.Language is null)
|
if (culture is not null && pathInfo.Language is null)
|
||||||
{
|
{
|
||||||
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
|
||||||
|
? culture.Name
|
||||||
|
: culture.ThreeLetterISOLanguageName;
|
||||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
else if (culture is not null && pathInfo.Language == "hin")
|
else if (culture is not null && pathInfo.Language == "hin")
|
||||||
{
|
{
|
||||||
// Hindi language code "hi" collides with a hearing impaired flag - use as Hindi only if no other language is set
|
// Hindi language code "hi" collides with a hearing impaired flag - use as Hindi only if no other language is set
|
||||||
pathInfo.IsHearingImpaired = true;
|
pathInfo.IsHearingImpaired = true;
|
||||||
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
|
||||||
|
? culture.Name
|
||||||
|
: culture.ThreeLetterISOLanguageName;
|
||||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
|
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
|||||||
@@ -1,43 +1,40 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Emby.Naming.TV
|
namespace Emby.Naming.TV
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class to parse season paths.
|
/// Class to parse season paths.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SeasonPathParser
|
public static partial class SeasonPathParser
|
||||||
{
|
{
|
||||||
/// <summary>
|
private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled);
|
||||||
/// A season folder must contain one of these somewhere in the name.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly string[] _seasonFolderNames =
|
|
||||||
{
|
|
||||||
"season",
|
|
||||||
"sæson",
|
|
||||||
"temporada",
|
|
||||||
"saison",
|
|
||||||
"staffel",
|
|
||||||
"series",
|
|
||||||
"сезон",
|
|
||||||
"stagione"
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly char[] _splitChars = ['.', '_', ' ', '-'];
|
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
||||||
|
private static partial Regex ProcessPre();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
||||||
|
private static partial Regex ProcessPost();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)]
|
||||||
|
private static partial Regex SeasonPrefix();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to parse season number from path.
|
/// Attempts to parse season number from path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to season.</param>
|
/// <param name="path">Path to season.</param>
|
||||||
|
/// <param name="parentPath">Folder name of the parent.</param>
|
||||||
/// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
|
/// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
|
||||||
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
|
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
|
||||||
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
|
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
|
||||||
public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
|
public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders)
|
||||||
{
|
{
|
||||||
var result = new SeasonPathParserResult();
|
var result = new SeasonPathParserResult();
|
||||||
|
var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
|
||||||
|
|
||||||
var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
|
var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders);
|
||||||
|
|
||||||
result.SeasonNumber = seasonNumber;
|
result.SeasonNumber = seasonNumber;
|
||||||
|
|
||||||
@@ -54,84 +51,70 @@ namespace Emby.Naming.TV
|
|||||||
/// Gets the season number from path.
|
/// Gets the season number from path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
|
/// <param name="parentFolderName">The parent folder name.</param>
|
||||||
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
|
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
|
||||||
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
|
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
|
||||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||||
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
|
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
|
||||||
string path,
|
string path,
|
||||||
|
string? parentFolderName,
|
||||||
bool supportSpecialAliases,
|
bool supportSpecialAliases,
|
||||||
bool supportNumericSeasonFolders)
|
bool supportNumericSeasonFolders)
|
||||||
{
|
{
|
||||||
string filename = Path.GetFileName(path);
|
var fileName = Path.GetFileName(path);
|
||||||
|
|
||||||
if (supportSpecialAliases)
|
var seasonPrefixMatch = SeasonPrefix().Match(fileName);
|
||||||
|
if (seasonPrefixMatch.Success &&
|
||||||
|
int.TryParse(seasonPrefixMatch.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||||
{
|
{
|
||||||
if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase))
|
return (val, true);
|
||||||
{
|
|
||||||
return (0, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return (0, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (supportNumericSeasonFolders)
|
string filename = CleanNameRegex.Replace(fileName, string.Empty);
|
||||||
|
|
||||||
|
if (parentFolderName is not null)
|
||||||
{
|
{
|
||||||
if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
var cleanParent = CleanNameRegex.Replace(parentFolderName, string.Empty);
|
||||||
{
|
filename = filename.Replace(cleanParent, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
return (val, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryGetSeasonNumberFromPart(filename, out int seasonNumber))
|
if (supportSpecialAliases &&
|
||||||
|
(filename.Equals("specials", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
filename.Equals("extras", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
return (seasonNumber, true);
|
return (0, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for one of the season folder names
|
if (supportNumericSeasonFolders &&
|
||||||
foreach (var name in _seasonFolderNames)
|
int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val))
|
||||||
{
|
{
|
||||||
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
|
return (val, true);
|
||||||
{
|
|
||||||
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (result.SeasonNumber.HasValue)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries);
|
var preMatch = ProcessPre().Match(filename);
|
||||||
foreach (var part in parts)
|
if (preMatch.Success)
|
||||||
{
|
{
|
||||||
if (TryGetSeasonNumberFromPart(part, out seasonNumber))
|
return CheckMatch(preMatch);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var postMatch = ProcessPost().Match(filename);
|
||||||
|
return CheckMatch(postMatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
|
||||||
|
{
|
||||||
|
var numberString = match.Groups["seasonnumber"];
|
||||||
|
if (numberString.Success)
|
||||||
|
{
|
||||||
|
if (int.TryParse(numberString.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber))
|
||||||
{
|
{
|
||||||
return (seasonNumber, true);
|
return (seasonNumber, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (null, true);
|
return (null, false);
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetSeasonNumberFromPart(ReadOnlySpan<char> part, out int seasonNumber)
|
|
||||||
{
|
|
||||||
seasonNumber = 0;
|
|
||||||
if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
|
||||||
{
|
|
||||||
seasonNumber = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace Emby.Naming.TV
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Regex that matches strings of at least 2 characters separated by a dot or underscore.
|
/// Regex that matches strings of at least 2 characters separated by a dot or underscore.
|
||||||
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
|
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
|
||||||
/// preserving namings like "S.H.O.W".
|
/// preserving names like "S.H.O.W".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
|
[GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
|
||||||
private static partial Regex SeriesNameRegex();
|
private static partial Regex SeriesNameRegex();
|
||||||
|
|||||||
@@ -18,68 +18,49 @@ namespace Emby.Naming.Video
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
|
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
|
||||||
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||||
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
|
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "")
|
||||||
{
|
{
|
||||||
var result = new ExtraResult();
|
ExtraResult result = new ExtraResult();
|
||||||
|
|
||||||
for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
|
bool isAudioFile = AudioFileParser.IsAudioFile(path, namingOptions);
|
||||||
|
bool isVideoFile = VideoResolver.IsVideoFile(path, namingOptions);
|
||||||
|
|
||||||
|
ReadOnlySpan<char> pathSpan = path.AsSpan();
|
||||||
|
ReadOnlySpan<char> fileName = Path.GetFileName(pathSpan);
|
||||||
|
ReadOnlySpan<char> fileNameWithoutExtension = Path.GetFileNameWithoutExtension(pathSpan);
|
||||||
|
// Trim the digits from the end of the filename so we can recognize things like -trailer2
|
||||||
|
ReadOnlySpan<char> trimmedFileNameWithoutExtension = fileNameWithoutExtension.TrimEnd(_digits);
|
||||||
|
ReadOnlySpan<char> directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
|
||||||
|
string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
|
||||||
|
|
||||||
|
foreach (ExtraRule rule in namingOptions.VideoExtraRules)
|
||||||
{
|
{
|
||||||
var rule = namingOptions.VideoExtraRules[i];
|
if ((rule.MediaType == MediaType.Audio && !isAudioFile)
|
||||||
if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
|
|| (rule.MediaType == MediaType.Video && !isVideoFile))
|
||||||
|| (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pathSpan = path.AsSpan();
|
bool isMatch = rule.RuleType switch
|
||||||
if (rule.RuleType == ExtraRuleType.Filename)
|
|
||||||
{
|
{
|
||||||
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
ExtraRuleType.Filename => fileNameWithoutExtension.Equals(rule.Token, StringComparison.OrdinalIgnoreCase),
|
||||||
|
ExtraRuleType.Suffix => trimmedFileNameWithoutExtension.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase),
|
||||||
|
ExtraRuleType.Regex => Regex.IsMatch(fileName, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||||
|
ExtraRuleType.DirectoryName => directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
if (!isMatch)
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.Suffix)
|
|
||||||
{
|
{
|
||||||
// Trim the digits from the end of the filename so we can recognize things like -trailer2
|
continue;
|
||||||
var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
|
|
||||||
|
|
||||||
if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.Regex)
|
|
||||||
{
|
|
||||||
var filename = Path.GetFileName(path.AsSpan());
|
|
||||||
|
|
||||||
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
|
||||||
|
|
||||||
if (isMatch)
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
|
||||||
{
|
|
||||||
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
|
|
||||||
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.ExtraType is not null)
|
result.ExtraType = rule.ExtraType;
|
||||||
{
|
result.Rule = rule;
|
||||||
return result;
|
return result;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ namespace Emby.Naming.Video
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class StackMetadata
|
private sealed class StackMetadata
|
||||||
{
|
{
|
||||||
public StackMetadata(bool isDirectory, bool isNumerical, string partType)
|
public StackMetadata(bool isDirectory, bool isNumerical, string partType)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ namespace Emby.Naming.Video
|
|||||||
/// <param name="namingOptions">The naming options.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
|
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
|
||||||
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
||||||
|
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
|
||||||
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
||||||
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
|
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "")
|
||||||
{
|
{
|
||||||
// Filter out all extras, otherwise they could cause stacks to not be resolved
|
// Filter out all extras, otherwise they could cause stacks to not be resolved
|
||||||
// See the unit test TestStackedWithTrailer
|
// See the unit test TestStackedWithTrailer
|
||||||
@@ -65,7 +66,7 @@ namespace Emby.Naming.Video
|
|||||||
{
|
{
|
||||||
var info = new VideoInfo(stack.Name)
|
var info = new VideoInfo(stack.Name)
|
||||||
{
|
{
|
||||||
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
|
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot))
|
||||||
.OfType<VideoFileInfo>()
|
.OfType<VideoFileInfo>()
|
||||||
.ToList()
|
.ToList()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ namespace Emby.Naming.Video
|
|||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
||||||
|
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
|
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
|
||||||
{
|
{
|
||||||
return Resolve(path, true, namingOptions, parseName);
|
return Resolve(path, true, namingOptions, parseName, libraryRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -28,10 +29,11 @@ namespace Emby.Naming.Video
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
|
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
|
public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions, string? libraryRoot = "")
|
||||||
{
|
{
|
||||||
return Resolve(path, false, namingOptions);
|
return Resolve(path, false, namingOptions, libraryRoot: libraryRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -41,9 +43,10 @@ namespace Emby.Naming.Video
|
|||||||
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
|
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
|
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
|
||||||
|
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
|
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
|
||||||
public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
|
public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
{
|
{
|
||||||
@@ -75,7 +78,7 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
||||||
|
|
||||||
var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
|
var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions, libraryRoot);
|
||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(path);
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
|
|||||||
if (item.IsFileProtocol)
|
if (item.IsFileProtocol)
|
||||||
{
|
{
|
||||||
var file = directoryService.GetFile(item.Path);
|
var file = directoryService.GetFile(item.Path);
|
||||||
return file is not null && file.LastWriteTimeUtc != item.DateModified;
|
return file is not null && item.HasChanged(file.LastWriteTimeUtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -108,7 +108,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
|
|||||||
var dateTaken = image.ImageTag.DateTime;
|
var dateTaken = image.ImageTag.DateTime;
|
||||||
if (dateTaken.HasValue)
|
if (dateTaken.HasValue)
|
||||||
{
|
{
|
||||||
item.DateCreated = dateTaken.Value;
|
item.DateCreated = dateTaken.Value.ToUniversalTime();
|
||||||
item.PremiereDate = dateTaken.Value;
|
item.PremiereDate = dateTaken.Value;
|
||||||
item.ProductionYear = dateTaken.Value.Year;
|
item.ProductionYear = dateTaken.Value.Year;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.AppBase
|
namespace Emby.Server.Implementations.AppBase
|
||||||
@@ -30,80 +33,101 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
ConfigurationDirectoryPath = configurationDirectoryPath;
|
ConfigurationDirectoryPath = configurationDirectoryPath;
|
||||||
CachePath = cacheDirectoryPath;
|
CachePath = cacheDirectoryPath;
|
||||||
WebPath = webDirectoryPath;
|
WebPath = webDirectoryPath;
|
||||||
|
|
||||||
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the path to the program data folder.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The program data path.</value>
|
|
||||||
public string ProgramDataPath { get; }
|
public string ProgramDataPath { get; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string WebPath { get; }
|
public string WebPath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the path to the system folder.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The path to the system folder.</value>
|
|
||||||
public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
|
public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the folder path to the data directory.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The data directory.</value>
|
|
||||||
public string DataPath { get; }
|
public string DataPath { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string VirtualDataPath => "%AppDataPath%";
|
public string VirtualDataPath => "%AppDataPath%";
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the image cache path.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The image cache path.</value>
|
|
||||||
public string ImageCachePath => Path.Combine(CachePath, "images");
|
public string ImageCachePath => Path.Combine(CachePath, "images");
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the path to the plugin directory.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The plugins path.</value>
|
|
||||||
public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
|
public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the path to the plugin configurations directory.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The plugin configurations path.</value>
|
|
||||||
public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
|
public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the path to the log directory.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The log directory path.</value>
|
|
||||||
public string LogDirectoryPath { get; }
|
public string LogDirectoryPath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the path to the application configuration root directory.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The configuration directory path.</value>
|
|
||||||
public string ConfigurationDirectoryPath { get; }
|
public string ConfigurationDirectoryPath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the path to the system configuration file.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The system configuration file path.</value>
|
|
||||||
public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
|
public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets or sets the folder path to the cache directory.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The cache directory.</value>
|
|
||||||
public string CachePath { get; set; }
|
public string CachePath { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the folder path to the temp directory within the cache folder.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The temp directory.</value>
|
|
||||||
public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
|
public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string TrickplayPath => Path.Combine(DataPath, "trickplay");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string BackupPath => Path.Combine(DataPath, "backups");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual void MakeSanityCheckOrThrow()
|
||||||
|
{
|
||||||
|
CreateAndCheckMarker(ConfigurationDirectoryPath, "config");
|
||||||
|
CreateAndCheckMarker(LogDirectoryPath, "log");
|
||||||
|
CreateAndCheckMarker(PluginsPath, "plugin");
|
||||||
|
CreateAndCheckMarker(ProgramDataPath, "data");
|
||||||
|
CreateAndCheckMarker(CachePath, "cache");
|
||||||
|
CreateAndCheckMarker(DataPath, "data");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void CreateAndCheckMarker(string path, string markerName, bool recursive = false)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
|
||||||
|
CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> GetMarkers(string path, bool recursive = false)
|
||||||
|
{
|
||||||
|
return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckOrCreateMarker(string path, string markerName, bool recursive = false)
|
||||||
|
{
|
||||||
|
string? otherMarkers = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => !Path.GetFileName(e.AsSpan()).Equals(markerName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Error while checking for marker files, assume none exist and keep going
|
||||||
|
// TODO: add some logging
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherMarkers is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Expected to find only {markerName} but found marker for {otherMarkers}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var markerPath = Path.Combine(path, markerName);
|
||||||
|
if (!File.Exists(markerPath))
|
||||||
|
{
|
||||||
|
FileHelper.CreateEmpty(markerPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Events;
|
using MediaBrowser.Common.Events;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
@@ -19,7 +20,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
public abstract class BaseConfigurationManager : IConfigurationManager
|
public abstract class BaseConfigurationManager : IConfigurationManager
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, object> _configurations = new();
|
private readonly ConcurrentDictionary<string, object> _configurations = new();
|
||||||
private readonly object _configurationSyncLock = new();
|
private readonly Lock _configurationSyncLock = new();
|
||||||
|
|
||||||
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
||||||
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
||||||
@@ -226,6 +227,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
|
|
||||||
Logger.LogInformation("Setting cache path: {Path}", cachePath);
|
Logger.LogInformation("Setting cache path: {Path}", cachePath);
|
||||||
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
|
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
|
||||||
|
CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using System.Security.Cryptography.X509Certificates;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using Emby.Photos;
|
using Emby.Photos;
|
||||||
|
using Emby.Server.Implementations.Chapters;
|
||||||
using Emby.Server.Implementations.Collections;
|
using Emby.Server.Implementations.Collections;
|
||||||
using Emby.Server.Implementations.Configuration;
|
using Emby.Server.Implementations.Configuration;
|
||||||
using Emby.Server.Implementations.Cryptography;
|
using Emby.Server.Implementations.Cryptography;
|
||||||
@@ -39,8 +40,10 @@ using Jellyfin.Drawing;
|
|||||||
using Jellyfin.MediaEncoding.Hls.Playlist;
|
using Jellyfin.MediaEncoding.Hls.Playlist;
|
||||||
using Jellyfin.Networking.Manager;
|
using Jellyfin.Networking.Manager;
|
||||||
using Jellyfin.Networking.Udp;
|
using Jellyfin.Networking.Udp;
|
||||||
using Jellyfin.Server.Implementations;
|
using Jellyfin.Server.Implementations.FullSystemBackup;
|
||||||
|
using Jellyfin.Server.Implementations.Item;
|
||||||
using Jellyfin.Server.Implementations.MediaSegments;
|
using Jellyfin.Server.Implementations.MediaSegments;
|
||||||
|
using Jellyfin.Server.Implementations.SystemBackupService;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Events;
|
using MediaBrowser.Common.Events;
|
||||||
@@ -56,10 +59,14 @@ using MediaBrowser.Controller.Configuration;
|
|||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.LibraryTaskScheduler;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.Lyrics;
|
using MediaBrowser.Controller.Lyrics;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
|
using MediaBrowser.Controller.MediaSegments;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
@@ -83,7 +90,6 @@ using MediaBrowser.Model.Net;
|
|||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using MediaBrowser.Model.System;
|
using MediaBrowser.Model.System;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using MediaBrowser.Providers.Chapters;
|
|
||||||
using MediaBrowser.Providers.Lyric;
|
using MediaBrowser.Providers.Lyric;
|
||||||
using MediaBrowser.Providers.Manager;
|
using MediaBrowser.Providers.Manager;
|
||||||
using MediaBrowser.Providers.Plugins.Tmdb;
|
using MediaBrowser.Providers.Plugins.Tmdb;
|
||||||
@@ -91,7 +97,6 @@ using MediaBrowser.Providers.Subtitles;
|
|||||||
using MediaBrowser.XbmcMetadata.Providers;
|
using MediaBrowser.XbmcMetadata.Providers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -266,8 +271,15 @@ namespace Emby.Server.Implementations
|
|||||||
? Environment.MachineName
|
? Environment.MachineName
|
||||||
: ConfigurationManager.Configuration.ServerName;
|
: ConfigurationManager.Configuration.ServerName;
|
||||||
|
|
||||||
|
public string RestoreBackupPath { get; set; }
|
||||||
|
|
||||||
public string ExpandVirtualPath(string path)
|
public string ExpandVirtualPath(string path)
|
||||||
{
|
{
|
||||||
|
if (path is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var appPaths = ApplicationPaths;
|
var appPaths = ApplicationPaths;
|
||||||
|
|
||||||
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
|
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -465,6 +477,7 @@ namespace Emby.Server.Implementations
|
|||||||
serviceCollection.AddSingleton<IApplicationHost>(this);
|
serviceCollection.AddSingleton<IApplicationHost>(this);
|
||||||
serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
|
serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
|
||||||
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||||
|
serviceCollection.AddSingleton<IBackupService, BackupService>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
|
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
|
||||||
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
|
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
|
||||||
@@ -492,13 +505,20 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
|
||||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>();
|
||||||
|
serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
|
||||||
|
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
|
||||||
|
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
|
||||||
|
serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
|
||||||
|
serviceCollection.AddSingleton<IKeyframeRepository, KeyframeRepository>();
|
||||||
|
serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||||
serviceCollection.AddSingleton<EncodingHelper>();
|
serviceCollection.AddSingleton<EncodingHelper>();
|
||||||
|
serviceCollection.AddSingleton<IPathManager, PathManager>();
|
||||||
|
serviceCollection.AddSingleton<IExternalDataManager, ExternalDataManager>();
|
||||||
|
|
||||||
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
||||||
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
||||||
@@ -533,6 +553,7 @@ namespace Emby.Server.Implementations
|
|||||||
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
|
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
||||||
|
serviceCollection.AddSingleton<ILimitedConcurrencyLibraryScheduler, LimitedConcurrencyLibraryScheduler>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
||||||
|
|
||||||
@@ -542,13 +563,12 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IAuthService, AuthService>();
|
serviceCollection.AddSingleton<IAuthService, AuthService>();
|
||||||
serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
|
serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
|
||||||
serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
|
serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
|
||||||
|
serviceCollection.AddSingleton<IKeyframeManager, KeyframeManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||||
|
|
||||||
@@ -565,23 +585,10 @@ namespace Emby.Server.Implementations
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create services registered with the service container that need to be initialized at application startup.
|
/// Create services registered with the service container that need to be initialized at application startup.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="startupConfig">The configuration used to initialise the application.</param>
|
||||||
/// <returns>A task representing the service initialization operation.</returns>
|
/// <returns>A task representing the service initialization operation.</returns>
|
||||||
public async Task InitializeServices()
|
public async Task InitializeServices(IConfiguration startupConfig)
|
||||||
{
|
{
|
||||||
var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
|
|
||||||
await using (jellyfinDb.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
|
|
||||||
{
|
|
||||||
Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
|
|
||||||
await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false);
|
|
||||||
Logger.LogInformation("EFCore migrations applied successfully");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
|
|
||||||
((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
|
|
||||||
|
|
||||||
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
|
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
|
||||||
await localizationManager.LoadAll().ConfigureAwait(false);
|
await localizationManager.LoadAll().ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -607,7 +614,7 @@ namespace Emby.Server.Implementations
|
|||||||
// Don't use an empty string password
|
// Don't use an empty string password
|
||||||
password = string.IsNullOrWhiteSpace(password) ? null : password;
|
password = string.IsNullOrWhiteSpace(password) ? null : password;
|
||||||
|
|
||||||
var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
|
var localCert = X509CertificateLoader.LoadPkcs12FromFile(path, password, X509KeyStorageFlags.UserKeySet);
|
||||||
if (!localCert.HasPrivateKey)
|
if (!localCert.HasPrivateKey)
|
||||||
{
|
{
|
||||||
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
|
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
|
||||||
@@ -629,23 +636,26 @@ namespace Emby.Server.Implementations
|
|||||||
private void SetStaticProperties()
|
private void SetStaticProperties()
|
||||||
{
|
{
|
||||||
// For now there's no real way to inject these properly
|
// For now there's no real way to inject these properly
|
||||||
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
BaseItem.ChapterManager = Resolve<IChapterManager>();
|
||||||
BaseItem.ConfigurationManager = ConfigurationManager;
|
|
||||||
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
|
||||||
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
|
||||||
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
|
||||||
BaseItem.ItemRepository = Resolve<IItemRepository>();
|
|
||||||
BaseItem.FileSystem = Resolve<IFileSystem>();
|
|
||||||
BaseItem.UserDataManager = Resolve<IUserDataManager>();
|
|
||||||
BaseItem.ChannelManager = Resolve<IChannelManager>();
|
BaseItem.ChannelManager = Resolve<IChannelManager>();
|
||||||
Video.RecordingsManager = Resolve<IRecordingsManager>();
|
BaseItem.ConfigurationManager = ConfigurationManager;
|
||||||
Folder.UserViewManager = Resolve<IUserViewManager>();
|
BaseItem.FileSystem = Resolve<IFileSystem>();
|
||||||
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
|
BaseItem.ItemRepository = Resolve<IItemRepository>();
|
||||||
UserView.CollectionManager = Resolve<ICollectionManager>();
|
BaseItem.LibraryManager = Resolve<ILibraryManager>();
|
||||||
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
|
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
||||||
|
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
|
||||||
BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>();
|
BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>();
|
||||||
|
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
|
||||||
|
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
||||||
|
BaseItem.UserDataManager = Resolve<IUserDataManager>();
|
||||||
CollectionFolder.XmlSerializer = _xmlSerializer;
|
CollectionFolder.XmlSerializer = _xmlSerializer;
|
||||||
CollectionFolder.ApplicationHost = this;
|
CollectionFolder.ApplicationHost = this;
|
||||||
|
Folder.UserViewManager = Resolve<IUserViewManager>();
|
||||||
|
Folder.CollectionManager = Resolve<ICollectionManager>();
|
||||||
|
Folder.LimitedConcurrencyLibraryScheduler = Resolve<ILimitedConcurrencyLibraryScheduler>();
|
||||||
|
Episode.MediaEncoder = Resolve<IMediaEncoder>();
|
||||||
|
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
|
||||||
|
Video.RecordingsManager = Resolve<IRecordingsManager>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
using MediaBrowser.Controller.Chapters;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
|
using MediaBrowser.Controller.Persistence;
|
||||||
|
using MediaBrowser.Controller.Providers;
|
||||||
|
using MediaBrowser.Model.Configuration;
|
||||||
|
using MediaBrowser.Model.Dto;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
using MediaBrowser.Model.MediaInfo;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Chapters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The chapter manager.
|
||||||
|
/// </summary>
|
||||||
|
public class ChapterManager : IChapterManager
|
||||||
|
{
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly ILogger<ChapterManager> _logger;
|
||||||
|
private readonly IMediaEncoder _encoder;
|
||||||
|
private readonly IChapterRepository _chapterRepository;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IPathManager _pathManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The first chapter ticks.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ChapterManager"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The <see cref="ILogger{ChapterManager}"/>.</param>
|
||||||
|
/// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
|
||||||
|
/// <param name="encoder">The <see cref="IMediaEncoder"/>.</param>
|
||||||
|
/// <param name="chapterRepository">The <see cref="IChapterRepository"/>.</param>
|
||||||
|
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
|
||||||
|
/// <param name="pathManager">The <see cref="IPathManager"/>.</param>
|
||||||
|
public ChapterManager(
|
||||||
|
ILogger<ChapterManager> logger,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
IMediaEncoder encoder,
|
||||||
|
IChapterRepository chapterRepository,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IPathManager pathManager)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_encoder = encoder;
|
||||||
|
_chapterRepository = chapterRepository;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_pathManager = pathManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether [is eligible for chapter image extraction] [the specified video].
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="video">The video.</param>
|
||||||
|
/// <param name="libraryOptions">The library options for the video.</param>
|
||||||
|
/// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns>
|
||||||
|
private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions)
|
||||||
|
{
|
||||||
|
if (video.IsPlaceHolder)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.IsShortcut)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!video.IsCompleteMedia)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't extract images if there are no video streams
|
||||||
|
return video.DefaultVideoStreamIndex.HasValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long GetAverageDurationBetweenChapters(IReadOnlyList<ChapterInfo> chapters)
|
||||||
|
{
|
||||||
|
if (chapters.Count < 2)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long sum = 0;
|
||||||
|
for (int i = 1; i < chapters.Count; i++)
|
||||||
|
{
|
||||||
|
sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum / chapters.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (chapters.Count == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var libraryOptions = _libraryManager.GetLibraryOptions(video);
|
||||||
|
|
||||||
|
if (!IsEligibleForChapterImageExtraction(video, libraryOptions))
|
||||||
|
{
|
||||||
|
extractImages = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
|
||||||
|
var threshold = TimeSpan.FromSeconds(1).Ticks;
|
||||||
|
if (averageChapterDuration < threshold)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold);
|
||||||
|
extractImages = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = true;
|
||||||
|
var changesMade = false;
|
||||||
|
|
||||||
|
var runtimeTicks = video.RunTimeTicks ?? 0;
|
||||||
|
|
||||||
|
var currentImages = GetSavedChapterImages(video, directoryService);
|
||||||
|
|
||||||
|
foreach (var chapter in chapters)
|
||||||
|
{
|
||||||
|
if (chapter.StartPositionTicks >= runtimeTicks)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime.", video.Name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = _pathManager.GetChapterImagePath(video, chapter.StartPositionTicks);
|
||||||
|
|
||||||
|
if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (extractImages)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Add some time for the first chapter to make sure we don't end up with a black image
|
||||||
|
var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks);
|
||||||
|
|
||||||
|
var inputPath = video.Path;
|
||||||
|
var directoryPath = Path.GetDirectoryName(path);
|
||||||
|
if (!string.IsNullOrEmpty(directoryPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var container = video.Container;
|
||||||
|
var mediaSource = new MediaSourceInfo
|
||||||
|
{
|
||||||
|
VideoType = video.VideoType,
|
||||||
|
IsoType = video.IsoType,
|
||||||
|
Protocol = video.PathProtocol ?? MediaProtocol.File,
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Extracting chapter image for {Name} at {Path}", video.Name, inputPath);
|
||||||
|
var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false);
|
||||||
|
File.Copy(tempFile, path, true);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_fileSystem.DeleteFile(tempFile);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.ImagePath = path;
|
||||||
|
chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path));
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(chapter.ImagePath))
|
||||||
|
{
|
||||||
|
chapter.ImagePath = null;
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
chapter.ImagePath = path;
|
||||||
|
chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
else if (libraryOptions?.EnableChapterImageExtraction != true)
|
||||||
|
{
|
||||||
|
// We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image
|
||||||
|
chapter.ImagePath = null;
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveChapters && changesMade)
|
||||||
|
{
|
||||||
|
SaveChapters(video, chapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteDeadImages(currentImages, chapters);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
|
||||||
|
{
|
||||||
|
// Remove any chapters that are outside of the runtime of the video
|
||||||
|
var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
|
||||||
|
_chapterRepository.SaveChapters(video.Id, validChapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ChapterInfo? GetChapter(Guid baseItemId, int index)
|
||||||
|
{
|
||||||
|
return _chapterRepository.GetChapter(baseItemId, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId)
|
||||||
|
{
|
||||||
|
return _chapterRepository.GetChapters(baseItemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task DeleteChapterDataAsync(Guid itemId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _chapterRepository.DeleteChaptersAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
|
||||||
|
{
|
||||||
|
var path = _pathManager.GetChapterImageFolderPath(video);
|
||||||
|
if (!Directory.Exists(path))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return directoryService.GetFilePaths(path);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteDeadImages(IEnumerable<string> images, IEnumerable<ChapterInfo> chapters)
|
||||||
|
{
|
||||||
|
var existingImages = chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i));
|
||||||
|
var deadImages = images
|
||||||
|
.Except(existingImages, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var image in deadImages)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Deleting dead chapter image {Path}", image);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_fileSystem.DeleteFile(image!);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting {Path}.", image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Collections;
|
using MediaBrowser.Controller.Collections;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
|
|
||||||
var libraryOptions = new LibraryOptions
|
var libraryOptions = new LibraryOptions
|
||||||
{
|
{
|
||||||
PathInfos = new[] { new MediaPathInfo(path) },
|
PathInfos = [new MediaPathInfo(path)],
|
||||||
EnableRealtimeMonitor = false,
|
EnableRealtimeMonitor = false,
|
||||||
SaveLocalMetadata = true
|
SaveLocalMetadata = true
|
||||||
};
|
};
|
||||||
@@ -104,6 +104,8 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
|
|
||||||
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
|
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_libraryManager.RootFolder.Children = null;
|
||||||
|
|
||||||
return FindFolders(path).First();
|
return FindFolders(path).First();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,15 +152,15 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(path);
|
var info = Directory.CreateDirectory(path);
|
||||||
|
|
||||||
var collection = new BoxSet
|
var collection = new BoxSet
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
Path = path,
|
Path = path,
|
||||||
IsLocked = options.IsLocked,
|
IsLocked = options.IsLocked,
|
||||||
ProviderIds = options.ProviderIds,
|
ProviderIds = options.ProviderIds,
|
||||||
DateCreated = DateTime.UtcNow
|
DateCreated = info.CreationTimeUtc,
|
||||||
|
DateModified = info.LastWriteTimeUtc
|
||||||
};
|
};
|
||||||
|
|
||||||
parentFolder.AddChild(collection);
|
parentFolder.AddChild(collection);
|
||||||
@@ -204,7 +206,7 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
{
|
{
|
||||||
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
|
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("No collection exists with the supplied Id");
|
throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<BaseItem>? itemList = null;
|
List<BaseItem>? itemList = null;
|
||||||
@@ -218,7 +220,7 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
|
|
||||||
if (item is null)
|
if (item is null)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("No item exists with the supplied Id");
|
throw new ArgumentException("No item exists with the supplied Id " + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentLinkedChildrenIds.Contains(id))
|
if (!currentLinkedChildrenIds.Contains(id))
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ namespace Emby.Server.Implementations
|
|||||||
{ DefaultRedirectKey, "web/" },
|
{ DefaultRedirectKey, "web/" },
|
||||||
{ FfmpegProbeSizeKey, "1G" },
|
{ FfmpegProbeSizeKey, "1G" },
|
||||||
{ FfmpegAnalyzeDurationKey, "200M" },
|
{ FfmpegAnalyzeDurationKey, "200M" },
|
||||||
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
|
|
||||||
{ BindToUnixSocketKey, bool.FalseString },
|
{ BindToUnixSocketKey, bool.FalseString },
|
||||||
{ SqliteCacheSizeKey, "20000" },
|
{ SqliteCacheSizeKey, "20000" },
|
||||||
{ FfmpegSkipValidationKey, bool.FalseString },
|
{ FfmpegSkipValidationKey, bool.FalseString },
|
||||||
|
|||||||
@@ -1,269 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using Jellyfin.Extensions;
|
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data
|
|
||||||
{
|
|
||||||
public abstract class BaseSqliteRepository : IDisposable
|
|
||||||
{
|
|
||||||
private bool _disposed = false;
|
|
||||||
private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
|
|
||||||
private SqliteConnection _writeConnection;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
protected BaseSqliteRepository(ILogger<BaseSqliteRepository> logger)
|
|
||||||
{
|
|
||||||
Logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the path to the DB file.
|
|
||||||
/// </summary>
|
|
||||||
protected string DbFilePath { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the logger.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The logger.</value>
|
|
||||||
protected ILogger<BaseSqliteRepository> Logger { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the cache size.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The cache size or null.</value>
|
|
||||||
protected virtual int? CacheSize => null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
|
|
||||||
/// </summary>
|
|
||||||
protected virtual string LockingMode => "NORMAL";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The journal mode.</value>
|
|
||||||
protected virtual string JournalMode => "WAL";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
|
|
||||||
/// The default (-1) is overridden to prevent unconstrained WAL size, as reported by users.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The journal size limit.</value>
|
|
||||||
protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the page size.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The page size or null.</value>
|
|
||||||
protected virtual int? PageSize => null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the temp store mode.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The temp store mode.</value>
|
|
||||||
/// <see cref="TempStoreMode"/>
|
|
||||||
protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the synchronous mode.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The synchronous mode or null.</value>
|
|
||||||
/// <see cref="SynchronousMode"/>
|
|
||||||
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
|
|
||||||
|
|
||||||
public virtual void Initialize()
|
|
||||||
{
|
|
||||||
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
|
||||||
using (var connection = GetConnection())
|
|
||||||
{
|
|
||||||
connection.Execute("VACUUM");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ManagedConnection GetConnection(bool readOnly = false)
|
|
||||||
{
|
|
||||||
if (!readOnly)
|
|
||||||
{
|
|
||||||
_writeLock.Wait();
|
|
||||||
if (_writeConnection is not null)
|
|
||||||
{
|
|
||||||
return new ManagedConnection(_writeConnection, _writeLock);
|
|
||||||
}
|
|
||||||
|
|
||||||
var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
|
|
||||||
writeConnection.Open();
|
|
||||||
|
|
||||||
if (CacheSize.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JournalSizeLimit.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Synchronous.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PageSize.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
|
||||||
|
|
||||||
return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
|
|
||||||
}
|
|
||||||
|
|
||||||
var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
|
|
||||||
connection.Open();
|
|
||||||
|
|
||||||
if (CacheSize.HasValue)
|
|
||||||
{
|
|
||||||
connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
|
||||||
{
|
|
||||||
connection.Execute("PRAGMA locking_mode=" + LockingMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
|
||||||
{
|
|
||||||
connection.Execute("PRAGMA journal_mode=" + JournalMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JournalSizeLimit.HasValue)
|
|
||||||
{
|
|
||||||
connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Synchronous.HasValue)
|
|
||||||
{
|
|
||||||
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PageSize.HasValue)
|
|
||||||
{
|
|
||||||
connection.Execute("PRAGMA page_size=" + PageSize.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
|
||||||
|
|
||||||
return new ManagedConnection(connection, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
|
|
||||||
{
|
|
||||||
var command = connection.CreateCommand();
|
|
||||||
command.CommandText = sql;
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected bool TableExists(ManagedConnection connection, string name)
|
|
||||||
{
|
|
||||||
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
|
||||||
{
|
|
||||||
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected List<string> GetColumnNames(ManagedConnection connection, string table)
|
|
||||||
{
|
|
||||||
var columnNames = new List<string>();
|
|
||||||
|
|
||||||
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
|
|
||||||
{
|
|
||||||
if (row.TryGetString(1, out var columnName))
|
|
||||||
{
|
|
||||||
columnNames.Add(columnName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return columnNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
|
||||||
{
|
|
||||||
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void CheckDisposed()
|
|
||||||
{
|
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases unmanaged and - optionally - managed resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
||||||
protected virtual void Dispose(bool dispose)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dispose)
|
|
||||||
{
|
|
||||||
_writeLock.Wait();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_writeConnection.Dispose();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_writeLock.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
_writeLock.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_writeConnection = null;
|
|
||||||
_writeLock = null;
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +1,119 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Database.Implementations;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data
|
namespace Emby.Server.Implementations.Data;
|
||||||
|
|
||||||
|
public class CleanDatabaseScheduledTask : ILibraryPostScanTask
|
||||||
{
|
{
|
||||||
public class CleanDatabaseScheduledTask : ILibraryPostScanTask
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
|
||||||
|
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||||
|
private readonly IPathManager _pathManager;
|
||||||
|
|
||||||
|
public CleanDatabaseScheduledTask(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
ILogger<CleanDatabaseScheduledTask> logger,
|
||||||
|
IDbContextFactory<JellyfinDbContext> dbProvider,
|
||||||
|
IPathManager pathManager)
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
|
_logger = logger;
|
||||||
|
_dbProvider = dbProvider;
|
||||||
|
_pathManager = pathManager;
|
||||||
|
}
|
||||||
|
|
||||||
public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger<CleanDatabaseScheduledTask> logger)
|
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
|
||||||
_logger = logger;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
|
||||||
|
{
|
||||||
|
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
CleanDeadItems(cancellationToken, progress);
|
HasDeadParentId = true
|
||||||
return Task.CompletedTask;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
|
var numComplete = 0;
|
||||||
|
var numItems = itemIds.Count + 1;
|
||||||
|
|
||||||
|
_logger.LogDebug("Cleaning {Number} items with dead parents", numItems);
|
||||||
|
|
||||||
|
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
|
||||||
|
|
||||||
|
foreach (var itemId in itemIds)
|
||||||
{
|
{
|
||||||
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
if (item is not null)
|
||||||
{
|
{
|
||||||
HasDeadParentId = true
|
_logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
|
||||||
});
|
|
||||||
|
|
||||||
var numComplete = 0;
|
foreach (var mediaSource in item.GetMediaSources(false))
|
||||||
var numItems = itemIds.Count;
|
|
||||||
|
|
||||||
_logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
|
|
||||||
|
|
||||||
foreach (var itemId in itemIds)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
|
||||||
|
|
||||||
if (item is not null)
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
|
// Delete extracted data
|
||||||
|
var mediaSourceItem = _libraryManager.GetItemById(mediaSource.Id);
|
||||||
_libraryManager.DeleteItem(item, new DeleteOptions
|
if (mediaSourceItem is null)
|
||||||
{
|
{
|
||||||
DeleteFileLocation = false
|
continue;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
var extractedDataFolders = _pathManager.GetExtractedDataPaths(mediaSourceItem);
|
||||||
|
foreach (var folder in extractedDataFolders)
|
||||||
|
{
|
||||||
|
if (Directory.Exists(folder))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(folder, true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to remove {Folder}: {Exception}", folder, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
numComplete++;
|
// Delete item
|
||||||
double percent = numComplete;
|
_libraryManager.DeleteItem(item, new DeleteOptions
|
||||||
percent /= numItems;
|
{
|
||||||
progress.Report(percent * 100);
|
DeleteFileLocation = false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(100);
|
numComplete++;
|
||||||
|
double percent = numComplete;
|
||||||
|
percent /= numItems;
|
||||||
|
subProgress.Report(percent * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
|
||||||
|
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
await using (context.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
await using (transaction.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
subProgress.Report(50);
|
||||||
|
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
subProgress.Report(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using Emby.Server.Implementations.Playlists;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
|
using MediaBrowser.Controller.LiveTv;
|
||||||
|
using MediaBrowser.Controller.Persistence;
|
||||||
|
using MediaBrowser.Controller.Playlists;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Data;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class ItemTypeLookup : IItemTypeLookup
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<string> MusicGenreTypes { get; } = [
|
||||||
|
typeof(Audio).FullName!,
|
||||||
|
typeof(MusicVideo).FullName!,
|
||||||
|
typeof(MusicAlbum).FullName!,
|
||||||
|
typeof(MusicArtist).FullName!,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyDictionary<BaseItemKind, string> BaseItemKindNames { get; } = new Dictionary<BaseItemKind, string>()
|
||||||
|
{
|
||||||
|
{ BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName! },
|
||||||
|
{ BaseItemKind.Audio, typeof(Audio).FullName! },
|
||||||
|
{ BaseItemKind.AudioBook, typeof(AudioBook).FullName! },
|
||||||
|
{ BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName! },
|
||||||
|
{ BaseItemKind.Book, typeof(Book).FullName! },
|
||||||
|
{ BaseItemKind.BoxSet, typeof(BoxSet).FullName! },
|
||||||
|
{ BaseItemKind.Channel, typeof(Channel).FullName! },
|
||||||
|
{ BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName! },
|
||||||
|
{ BaseItemKind.Episode, typeof(Episode).FullName! },
|
||||||
|
{ BaseItemKind.Folder, typeof(Folder).FullName! },
|
||||||
|
{ BaseItemKind.Genre, typeof(Genre).FullName! },
|
||||||
|
{ BaseItemKind.Movie, typeof(Movie).FullName! },
|
||||||
|
{ BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName! },
|
||||||
|
{ BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName! },
|
||||||
|
{ BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName! },
|
||||||
|
{ BaseItemKind.MusicArtist, typeof(MusicArtist).FullName! },
|
||||||
|
{ BaseItemKind.MusicGenre, typeof(MusicGenre).FullName! },
|
||||||
|
{ BaseItemKind.MusicVideo, typeof(MusicVideo).FullName! },
|
||||||
|
{ BaseItemKind.Person, typeof(Person).FullName! },
|
||||||
|
{ BaseItemKind.Photo, typeof(Photo).FullName! },
|
||||||
|
{ BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName! },
|
||||||
|
{ BaseItemKind.Playlist, typeof(Playlist).FullName! },
|
||||||
|
{ BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName! },
|
||||||
|
{ BaseItemKind.Season, typeof(Season).FullName! },
|
||||||
|
{ BaseItemKind.Series, typeof(Series).FullName! },
|
||||||
|
{ BaseItemKind.Studio, typeof(Studio).FullName! },
|
||||||
|
{ BaseItemKind.Trailer, typeof(Trailer).FullName! },
|
||||||
|
{ BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName! },
|
||||||
|
{ BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName! },
|
||||||
|
{ BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName! },
|
||||||
|
{ BaseItemKind.UserView, typeof(UserView).FullName! },
|
||||||
|
{ BaseItemKind.Video, typeof(Video).FullName! },
|
||||||
|
{ BaseItemKind.Year, typeof(Year).FullName! }
|
||||||
|
}.ToFrozenDictionary();
|
||||||
|
}
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data;
|
|
||||||
|
|
||||||
public sealed class ManagedConnection : IDisposable
|
|
||||||
{
|
|
||||||
private readonly SemaphoreSlim? _writeLock;
|
|
||||||
|
|
||||||
private SqliteConnection _db;
|
|
||||||
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
|
|
||||||
{
|
|
||||||
_db = db;
|
|
||||||
_writeLock = writeLock;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SqliteTransaction BeginTransaction()
|
|
||||||
=> _db.BeginTransaction();
|
|
||||||
|
|
||||||
public SqliteCommand CreateCommand()
|
|
||||||
=> _db.CreateCommand();
|
|
||||||
|
|
||||||
public void Execute(string commandText)
|
|
||||||
=> _db.Execute(commandText);
|
|
||||||
|
|
||||||
public SqliteCommand PrepareStatement(string sql)
|
|
||||||
=> _db.PrepareStatement(sql);
|
|
||||||
|
|
||||||
public IEnumerable<SqliteDataReader> Query(string commandText)
|
|
||||||
=> _db.Query(commandText);
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_writeLock is null)
|
|
||||||
{
|
|
||||||
// Read connections are managed with an internal pool
|
|
||||||
_db.Dispose();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Write lock is managed by BaseSqliteRepository
|
|
||||||
// Don't dispose here
|
|
||||||
_writeLock.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
_db = null!;
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -127,8 +127,16 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
result = reader.GetGuid(index);
|
try
|
||||||
return true;
|
{
|
||||||
|
result = reader.GetGuid(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
result = Guid.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
|
public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,369 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using Jellyfin.Data.Entities;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Persistence;
|
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data
|
|
||||||
{
|
|
||||||
public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
|
|
||||||
{
|
|
||||||
private readonly IUserManager _userManager;
|
|
||||||
|
|
||||||
public SqliteUserDataRepository(
|
|
||||||
ILogger<SqliteUserDataRepository> logger,
|
|
||||||
IServerConfigurationManager config,
|
|
||||||
IUserManager userManager)
|
|
||||||
: base(logger)
|
|
||||||
{
|
|
||||||
_userManager = userManager;
|
|
||||||
|
|
||||||
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Opens the connection to the database.
|
|
||||||
/// </summary>
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
|
||||||
{
|
|
||||||
var userDatasTableExists = TableExists(connection, "UserDatas");
|
|
||||||
var userDataTableExists = TableExists(connection, "userdata");
|
|
||||||
|
|
||||||
var users = userDatasTableExists ? null : _userManager.Users;
|
|
||||||
using var transaction = connection.BeginTransaction();
|
|
||||||
connection.Execute(string.Join(
|
|
||||||
';',
|
|
||||||
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
|
||||||
"drop index if exists idx_userdata",
|
|
||||||
"drop index if exists idx_userdata1",
|
|
||||||
"drop index if exists idx_userdata2",
|
|
||||||
"drop index if exists userdataindex1",
|
|
||||||
"drop index if exists userdataindex",
|
|
||||||
"drop index if exists userdataindex3",
|
|
||||||
"drop index if exists userdataindex4",
|
|
||||||
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
|
|
||||||
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
|
|
||||||
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
|
|
||||||
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
|
|
||||||
"create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
|
|
||||||
|
|
||||||
if (!userDataTableExists)
|
|
||||||
{
|
|
||||||
transaction.Commit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingColumnNames = GetColumnNames(connection, "userdata");
|
|
||||||
|
|
||||||
AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
|
|
||||||
AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
|
||||||
AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
|
||||||
|
|
||||||
if (userDatasTableExists)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportUserIds(connection, users);
|
|
||||||
|
|
||||||
connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
|
|
||||||
|
|
||||||
transaction.Commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
|
|
||||||
{
|
|
||||||
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
|
||||||
|
|
||||||
using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId"))
|
|
||||||
{
|
|
||||||
foreach (var user in users)
|
|
||||||
{
|
|
||||||
if (!userIdsWithUserData.Contains(user.Id))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
statement.TryBind("@UserId", user.Id);
|
|
||||||
statement.TryBind("@InternalUserId", user.InternalId);
|
|
||||||
|
|
||||||
statement.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
|
|
||||||
{
|
|
||||||
var list = new List<Guid>();
|
|
||||||
|
|
||||||
using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null"))
|
|
||||||
{
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
list.Add(row.GetGuid(0));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error while getting user");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(userData);
|
|
||||||
|
|
||||||
if (userId <= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
|
||||||
|
|
||||||
PersistUserData(userId, key, userData, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(userData);
|
|
||||||
|
|
||||||
if (userId <= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
PersistAllUserData(userId, userData, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Persists the user data.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="internalUserId">The user id.</param>
|
|
||||||
/// <param name="key">The key.</param>
|
|
||||||
/// <param name="userData">The user data.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
|
||||||
using (var transaction = connection.BeginTransaction())
|
|
||||||
{
|
|
||||||
SaveUserData(connection, internalUserId, key, userData);
|
|
||||||
transaction.Commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
|
|
||||||
{
|
|
||||||
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
|
||||||
{
|
|
||||||
statement.TryBind("@userId", internalUserId);
|
|
||||||
statement.TryBind("@key", key);
|
|
||||||
|
|
||||||
if (userData.Rating.HasValue)
|
|
||||||
{
|
|
||||||
statement.TryBind("@rating", userData.Rating.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statement.TryBindNull("@rating");
|
|
||||||
}
|
|
||||||
|
|
||||||
statement.TryBind("@played", userData.Played);
|
|
||||||
statement.TryBind("@playCount", userData.PlayCount);
|
|
||||||
statement.TryBind("@isFavorite", userData.IsFavorite);
|
|
||||||
statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks);
|
|
||||||
|
|
||||||
if (userData.LastPlayedDate.HasValue)
|
|
||||||
{
|
|
||||||
statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statement.TryBindNull("@lastPlayedDate");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userData.AudioStreamIndex.HasValue)
|
|
||||||
{
|
|
||||||
statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statement.TryBindNull("@AudioStreamIndex");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userData.SubtitleStreamIndex.HasValue)
|
|
||||||
{
|
|
||||||
statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
statement.TryBindNull("@SubtitleStreamIndex");
|
|
||||||
}
|
|
||||||
|
|
||||||
statement.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Persist all user data for the specified user.
|
|
||||||
/// </summary>
|
|
||||||
private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
|
||||||
using (var transaction = connection.BeginTransaction())
|
|
||||||
{
|
|
||||||
foreach (var userItemData in userDataList)
|
|
||||||
{
|
|
||||||
SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.Commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user data.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">The user id.</param>
|
|
||||||
/// <param name="key">The key.</param>
|
|
||||||
/// <returns>Task{UserItemData}.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">
|
|
||||||
/// userId
|
|
||||||
/// or
|
|
||||||
/// key.
|
|
||||||
/// </exception>
|
|
||||||
public UserItemData GetUserData(long userId, string key)
|
|
||||||
{
|
|
||||||
if (userId <= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
|
||||||
|
|
||||||
using (var connection = GetConnection(true))
|
|
||||||
{
|
|
||||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
|
||||||
{
|
|
||||||
statement.TryBind("@UserId", userId);
|
|
||||||
statement.TryBind("@Key", key);
|
|
||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
|
||||||
{
|
|
||||||
return ReadRow(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public UserItemData GetUserData(long userId, List<string> keys)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(keys);
|
|
||||||
|
|
||||||
if (keys.Count == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetUserData(userId, keys[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return all user-data associated with the given user.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">The internal user id.</param>
|
|
||||||
/// <returns>The list of user item data.</returns>
|
|
||||||
public List<UserItemData> GetAllUserData(long userId)
|
|
||||||
{
|
|
||||||
if (userId <= 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
var list = new List<UserItemData>();
|
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
|
||||||
{
|
|
||||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
|
|
||||||
{
|
|
||||||
statement.TryBind("@UserId", userId);
|
|
||||||
|
|
||||||
foreach (var row in statement.ExecuteQuery())
|
|
||||||
{
|
|
||||||
list.Add(ReadRow(row));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Read a row from the specified reader into the provided userData object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reader">The list of result set values.</param>
|
|
||||||
/// <returns>The user item data.</returns>
|
|
||||||
private UserItemData ReadRow(SqliteDataReader reader)
|
|
||||||
{
|
|
||||||
var userData = new UserItemData
|
|
||||||
{
|
|
||||||
Key = reader.GetString(0)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (reader.TryGetDouble(2, out var rating))
|
|
||||||
{
|
|
||||||
userData.Rating = rating;
|
|
||||||
}
|
|
||||||
|
|
||||||
userData.Played = reader.GetBoolean(3);
|
|
||||||
userData.PlayCount = reader.GetInt32(4);
|
|
||||||
userData.IsFavorite = reader.GetBoolean(5);
|
|
||||||
userData.PlaybackPositionTicks = reader.GetInt64(6);
|
|
||||||
|
|
||||||
if (reader.TryReadDateTime(7, out var lastPlayedDate))
|
|
||||||
{
|
|
||||||
userData.LastPlayedDate = lastPlayedDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.TryGetInt32(8, out var audioStreamIndex))
|
|
||||||
{
|
|
||||||
userData.AudioStreamIndex = audioStreamIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.TryGetInt32(9, out var subtitleStreamIndex))
|
|
||||||
{
|
|
||||||
userData.SubtitleStreamIndex = subtitleStreamIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
return userData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
namespace Emby.Server.Implementations.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The disk synchronization mode, controls how aggressively SQLite will write data
|
|
||||||
/// all the way out to physical storage.
|
|
||||||
/// </summary>
|
|
||||||
public enum SynchronousMode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// SQLite continues without syncing as soon as it has handed data off to the operating system.
|
|
||||||
/// </summary>
|
|
||||||
Off = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SQLite database engine will still sync at the most critical moments.
|
|
||||||
/// </summary>
|
|
||||||
Normal = 1,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// SQLite database engine will use the xSync method of the VFS
|
|
||||||
/// to ensure that all content is safely written to the disk surface prior to continuing.
|
|
||||||
/// </summary>
|
|
||||||
Full = 2,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
|
|
||||||
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
|
|
||||||
/// </summary>
|
|
||||||
Extra = 3
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
namespace Emby.Server.Implementations.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Storage mode used by temporary database files.
|
|
||||||
/// </summary>
|
|
||||||
public enum TempStoreMode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
|
|
||||||
/// is used to determine where temporary tables and indices are stored.
|
|
||||||
/// </summary>
|
|
||||||
Default = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Temporary tables and indices are stored in a file.
|
|
||||||
/// </summary>
|
|
||||||
File = 1,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
|
|
||||||
/// </summary>
|
|
||||||
Memory = 2
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ using System;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Devices
|
|||||||
{
|
{
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
private readonly ILogger<DeviceId> _logger;
|
private readonly ILogger<DeviceId> _logger;
|
||||||
private readonly object _syncLock = new object();
|
private readonly Lock _syncLock = new();
|
||||||
|
|
||||||
private string? _id;
|
private string? _id;
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Data.Entities;
|
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
|
using MediaBrowser.Controller.Chapters;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.Persistence;
|
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Controller.Trickplay;
|
using MediaBrowser.Controller.Trickplay;
|
||||||
@@ -37,10 +38,80 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
{
|
{
|
||||||
public class DtoService : IDtoService
|
public class DtoService : IDtoService
|
||||||
{
|
{
|
||||||
|
private static readonly FrozenDictionary<BaseItemKind, BaseItemKind[]> _relatedItemKinds = new Dictionary<BaseItemKind, BaseItemKind[]>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
BaseItemKind.Genre, [
|
||||||
|
BaseItemKind.Audio,
|
||||||
|
BaseItemKind.Episode,
|
||||||
|
BaseItemKind.Movie,
|
||||||
|
BaseItemKind.LiveTvProgram,
|
||||||
|
BaseItemKind.MusicAlbum,
|
||||||
|
BaseItemKind.MusicArtist,
|
||||||
|
BaseItemKind.MusicVideo,
|
||||||
|
BaseItemKind.Series,
|
||||||
|
BaseItemKind.Trailer
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BaseItemKind.MusicArtist, [
|
||||||
|
BaseItemKind.Audio,
|
||||||
|
BaseItemKind.MusicAlbum,
|
||||||
|
BaseItemKind.MusicVideo
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BaseItemKind.MusicGenre, [
|
||||||
|
BaseItemKind.Audio,
|
||||||
|
BaseItemKind.MusicAlbum,
|
||||||
|
BaseItemKind.MusicArtist,
|
||||||
|
BaseItemKind.MusicVideo
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BaseItemKind.Person, [
|
||||||
|
BaseItemKind.Audio,
|
||||||
|
BaseItemKind.Episode,
|
||||||
|
BaseItemKind.Movie,
|
||||||
|
BaseItemKind.LiveTvProgram,
|
||||||
|
BaseItemKind.MusicAlbum,
|
||||||
|
BaseItemKind.MusicArtist,
|
||||||
|
BaseItemKind.MusicVideo,
|
||||||
|
BaseItemKind.Series,
|
||||||
|
BaseItemKind.Trailer
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BaseItemKind.Studio, [
|
||||||
|
BaseItemKind.Audio,
|
||||||
|
BaseItemKind.Episode,
|
||||||
|
BaseItemKind.Movie,
|
||||||
|
BaseItemKind.LiveTvProgram,
|
||||||
|
BaseItemKind.MusicAlbum,
|
||||||
|
BaseItemKind.MusicArtist,
|
||||||
|
BaseItemKind.MusicVideo,
|
||||||
|
BaseItemKind.Series,
|
||||||
|
BaseItemKind.Trailer
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BaseItemKind.Year, [
|
||||||
|
BaseItemKind.Audio,
|
||||||
|
BaseItemKind.Episode,
|
||||||
|
BaseItemKind.Movie,
|
||||||
|
BaseItemKind.LiveTvProgram,
|
||||||
|
BaseItemKind.MusicAlbum,
|
||||||
|
BaseItemKind.MusicArtist,
|
||||||
|
BaseItemKind.MusicVideo,
|
||||||
|
BaseItemKind.Series,
|
||||||
|
BaseItemKind.Trailer
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}.ToFrozenDictionary();
|
||||||
|
|
||||||
private readonly ILogger<DtoService> _logger;
|
private readonly ILogger<DtoService> _logger;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IUserDataManager _userDataRepository;
|
private readonly IUserDataManager _userDataRepository;
|
||||||
private readonly IItemRepository _itemRepo;
|
|
||||||
|
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
private readonly IProviderManager _providerManager;
|
private readonly IProviderManager _providerManager;
|
||||||
@@ -51,24 +122,24 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
|
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
|
||||||
|
|
||||||
private readonly ITrickplayManager _trickplayManager;
|
private readonly ITrickplayManager _trickplayManager;
|
||||||
|
private readonly IChapterManager _chapterManager;
|
||||||
|
|
||||||
public DtoService(
|
public DtoService(
|
||||||
ILogger<DtoService> logger,
|
ILogger<DtoService> logger,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IUserDataManager userDataRepository,
|
IUserDataManager userDataRepository,
|
||||||
IItemRepository itemRepo,
|
|
||||||
IImageProcessor imageProcessor,
|
IImageProcessor imageProcessor,
|
||||||
IProviderManager providerManager,
|
IProviderManager providerManager,
|
||||||
IRecordingsManager recordingsManager,
|
IRecordingsManager recordingsManager,
|
||||||
IApplicationHost appHost,
|
IApplicationHost appHost,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
Lazy<ILiveTvManager> livetvManagerFactory,
|
Lazy<ILiveTvManager> livetvManagerFactory,
|
||||||
ITrickplayManager trickplayManager)
|
ITrickplayManager trickplayManager,
|
||||||
|
IChapterManager chapterManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_userDataRepository = userDataRepository;
|
_userDataRepository = userDataRepository;
|
||||||
_itemRepo = itemRepo;
|
|
||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
_providerManager = providerManager;
|
_providerManager = providerManager;
|
||||||
_recordingsManager = recordingsManager;
|
_recordingsManager = recordingsManager;
|
||||||
@@ -76,6 +147,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
_livetvManagerFactory = livetvManagerFactory;
|
_livetvManagerFactory = livetvManagerFactory;
|
||||||
_trickplayManager = trickplayManager;
|
_trickplayManager = trickplayManager;
|
||||||
|
_chapterManager = chapterManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
||||||
@@ -95,28 +167,16 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
if (item is LiveTvChannel tvChannel)
|
if (item is LiveTvChannel tvChannel)
|
||||||
{
|
{
|
||||||
(channelTuples ??= new()).Add((dto, tvChannel));
|
(channelTuples ??= []).Add((dto, tvChannel));
|
||||||
}
|
}
|
||||||
else if (item is LiveTvProgram)
|
else if (item is LiveTvProgram)
|
||||||
{
|
{
|
||||||
(programTuples ??= new()).Add((item, dto));
|
(programTuples ??= []).Add((item, dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is IItemByName byName)
|
if (options.ContainsField(ItemFields.ItemCounts))
|
||||||
{
|
{
|
||||||
if (options.ContainsField(ItemFields.ItemCounts))
|
SetItemByNameInfo(dto, user);
|
||||||
{
|
|
||||||
var libraryItems = byName.GetTaggedItems(new InternalItemsQuery(user)
|
|
||||||
{
|
|
||||||
Recursive = true,
|
|
||||||
DtoOptions = new DtoOptions(false)
|
|
||||||
{
|
|
||||||
EnableImages = false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
SetItemByNameInfo(item, dto, libraryItems);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
returnItems[index] = dto;
|
returnItems[index] = dto;
|
||||||
@@ -147,34 +207,14 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
|
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is IItemByName itemByName
|
if (options.ContainsField(ItemFields.ItemCounts))
|
||||||
&& options.ContainsField(ItemFields.ItemCounts))
|
|
||||||
{
|
{
|
||||||
SetItemByNameInfo(
|
SetItemByNameInfo(dto, user);
|
||||||
item,
|
|
||||||
dto,
|
|
||||||
GetTaggedItems(
|
|
||||||
itemByName,
|
|
||||||
user,
|
|
||||||
new DtoOptions(false)
|
|
||||||
{
|
|
||||||
EnableImages = false
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
|
|
||||||
{
|
|
||||||
return byName.GetTaggedItems(
|
|
||||||
new InternalItemsQuery(user)
|
|
||||||
{
|
|
||||||
Recursive = true,
|
|
||||||
DtoOptions = options
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
|
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
|
||||||
{
|
{
|
||||||
var dto = new BaseItemDto
|
var dto = new BaseItemDto
|
||||||
@@ -315,11 +355,15 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
/// TODO refactor this to use the new SetItemByNameInfo.
|
||||||
|
/// Some callers already have the counts extracted so no reason to retrieve them again.
|
||||||
public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user = null)
|
public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user = null)
|
||||||
{
|
{
|
||||||
var dto = GetBaseItemDtoInternal(item, options, user);
|
var dto = GetBaseItemDtoInternal(item, options, user);
|
||||||
|
|
||||||
if (taggedItems is not null && options.ContainsField(ItemFields.ItemCounts))
|
if (options.ContainsField(ItemFields.ItemCounts)
|
||||||
|
&& taggedItems is not null
|
||||||
|
&& taggedItems.Count != 0)
|
||||||
{
|
{
|
||||||
SetItemByNameInfo(item, dto, taggedItems);
|
SetItemByNameInfo(item, dto, taggedItems);
|
||||||
}
|
}
|
||||||
@@ -327,7 +371,58 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems)
|
private void SetItemByNameInfo(BaseItemDto dto, User? user)
|
||||||
|
{
|
||||||
|
if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = new InternalItemsQuery(user)
|
||||||
|
{
|
||||||
|
Recursive = true,
|
||||||
|
DtoOptions = new DtoOptions(false) { EnableImages = false },
|
||||||
|
IncludeItemTypes = relatedItemKinds
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (dto.Type)
|
||||||
|
{
|
||||||
|
case BaseItemKind.Genre:
|
||||||
|
case BaseItemKind.MusicGenre:
|
||||||
|
query.GenreIds = [dto.Id];
|
||||||
|
break;
|
||||||
|
case BaseItemKind.MusicArtist:
|
||||||
|
query.ArtistIds = [dto.Id];
|
||||||
|
break;
|
||||||
|
case BaseItemKind.Person:
|
||||||
|
query.PersonIds = [dto.Id];
|
||||||
|
break;
|
||||||
|
case BaseItemKind.Studio:
|
||||||
|
query.StudioIds = [dto.Id];
|
||||||
|
break;
|
||||||
|
case BaseItemKind.Year
|
||||||
|
when int.TryParse(dto.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year):
|
||||||
|
query.Years = [year];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var counts = _libraryManager.GetItemCounts(query);
|
||||||
|
|
||||||
|
dto.AlbumCount = counts.AlbumCount;
|
||||||
|
dto.ArtistCount = counts.ArtistCount;
|
||||||
|
dto.EpisodeCount = counts.EpisodeCount;
|
||||||
|
dto.MovieCount = counts.MovieCount;
|
||||||
|
dto.MusicVideoCount = counts.MusicVideoCount;
|
||||||
|
dto.ProgramCount = counts.ProgramCount;
|
||||||
|
dto.SeriesCount = counts.SeriesCount;
|
||||||
|
dto.SongCount = counts.SongCount;
|
||||||
|
dto.TrailerCount = counts.TrailerCount;
|
||||||
|
dto.ChildCount = counts.TotalItemCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
|
||||||
{
|
{
|
||||||
if (item is MusicArtist)
|
if (item is MusicArtist)
|
||||||
{
|
{
|
||||||
@@ -586,12 +681,12 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
if (dto.ImageBlurHashes is not null)
|
if (dto.ImageBlurHashes is not null)
|
||||||
{
|
{
|
||||||
// Only add BlurHash for the person's image.
|
// Only add BlurHash for the person's image.
|
||||||
baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
|
baseItemPerson.ImageBlurHashes = [];
|
||||||
foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
|
foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
|
||||||
{
|
{
|
||||||
if (blurHash is not null)
|
if (blurHash is not null)
|
||||||
{
|
{
|
||||||
baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
|
baseItemPerson.ImageBlurHashes[imageType] = [];
|
||||||
foreach (var (imageId, blurHashValue) in blurHash)
|
foreach (var (imageId, blurHashValue) in blurHash)
|
||||||
{
|
{
|
||||||
if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -670,11 +765,11 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(image.BlurHash))
|
if (!string.IsNullOrEmpty(image.BlurHash))
|
||||||
{
|
{
|
||||||
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
|
dto.ImageBlurHashes ??= [];
|
||||||
|
|
||||||
if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
|
if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
|
||||||
{
|
{
|
||||||
value = new Dictionary<string, string>();
|
value = [];
|
||||||
dto.ImageBlurHashes[image.Type] = value;
|
dto.ImageBlurHashes[image.Type] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,7 +800,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
if (hashes.Count > 0)
|
if (hashes.Count > 0)
|
||||||
{
|
{
|
||||||
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
|
dto.ImageBlurHashes ??= [];
|
||||||
|
|
||||||
dto.ImageBlurHashes[imageType] = hashes;
|
dto.ImageBlurHashes[imageType] = hashes;
|
||||||
}
|
}
|
||||||
@@ -752,7 +847,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
dto.AspectRatio = hasAspectRatio.AspectRatio;
|
dto.AspectRatio = hasAspectRatio.AspectRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
|
dto.ImageBlurHashes = [];
|
||||||
|
|
||||||
var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
|
var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
|
||||||
if (backdropLimit > 0)
|
if (backdropLimit > 0)
|
||||||
@@ -768,7 +863,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
if (options.EnableImages)
|
if (options.EnableImages)
|
||||||
{
|
{
|
||||||
dto.ImageTags = new Dictionary<ImageType, string>();
|
dto.ImageTags = [];
|
||||||
|
|
||||||
// Prevent implicitly captured closure
|
// Prevent implicitly captured closure
|
||||||
var currentItem = item;
|
var currentItem = item;
|
||||||
@@ -956,31 +1051,16 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
// Include artists that are not in the database yet, e.g., just added via metadata editor
|
// Include artists that are not in the database yet, e.g., just added via metadata editor
|
||||||
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
||||||
|
var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
||||||
|
|
||||||
dto.ArtistItems = hasArtist.Artists
|
dto.ArtistItems = hasArtist.Artists
|
||||||
// .Except(foundArtists, new DistinctNameComparer())
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
.Select(i =>
|
.Distinct()
|
||||||
{
|
.Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0
|
||||||
// This should not be necessary but we're seeing some cases of it
|
? new NameGuidPair { Name = name, Id = artists[0].Id }
|
||||||
if (string.IsNullOrEmpty(i))
|
: null)
|
||||||
{
|
.Where(item => item is not null)
|
||||||
return null;
|
.ToArray();
|
||||||
}
|
|
||||||
|
|
||||||
var artist = _libraryManager.GetArtist(i, new DtoOptions(false)
|
|
||||||
{
|
|
||||||
EnableImages = false
|
|
||||||
});
|
|
||||||
if (artist is not null)
|
|
||||||
{
|
|
||||||
return new NameGuidPair
|
|
||||||
{
|
|
||||||
Name = artist.Name,
|
|
||||||
Id = artist.Id
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}).Where(i => i is not null).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item is IHasAlbumArtist hasAlbumArtist)
|
if (item is IHasAlbumArtist hasAlbumArtist)
|
||||||
@@ -1005,31 +1085,16 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
// })
|
// })
|
||||||
// .ToList();
|
// .ToList();
|
||||||
|
|
||||||
|
var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
|
||||||
|
|
||||||
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
|
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
|
||||||
// .Except(foundArtists, new DistinctNameComparer())
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
.Select(i =>
|
.Distinct()
|
||||||
{
|
.Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0
|
||||||
// This should not be necessary but we're seeing some cases of it
|
? new NameGuidPair { Name = name, Id = albumArtists[0].Id }
|
||||||
if (string.IsNullOrEmpty(i))
|
: null)
|
||||||
{
|
.Where(item => item is not null)
|
||||||
return null;
|
.ToArray();
|
||||||
}
|
|
||||||
|
|
||||||
var artist = _libraryManager.GetArtist(i, new DtoOptions(false)
|
|
||||||
{
|
|
||||||
EnableImages = false
|
|
||||||
});
|
|
||||||
if (artist is not null)
|
|
||||||
{
|
|
||||||
return new NameGuidPair
|
|
||||||
{
|
|
||||||
Name = artist.Name,
|
|
||||||
Id = artist.Id
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}).Where(i => i is not null).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add video info
|
// Add video info
|
||||||
@@ -1060,12 +1125,17 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
if (options.ContainsField(ItemFields.Chapters))
|
if (options.ContainsField(ItemFields.Chapters))
|
||||||
{
|
{
|
||||||
dto.Chapters = _itemRepo.GetChapters(item);
|
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.Trickplay))
|
if (options.ContainsField(ItemFields.Trickplay))
|
||||||
{
|
{
|
||||||
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
||||||
|
dto.Trickplay = trickplay.ToDictionary(
|
||||||
|
mediaStream => mediaStream.Key,
|
||||||
|
mediaStream => mediaStream.Value.ToDictionary(
|
||||||
|
width => width.Key,
|
||||||
|
width => new TrickplayInfoDto(width.Value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
dto.ExtraType = video.ExtraType;
|
dto.ExtraType = video.ExtraType;
|
||||||
|
|||||||
@@ -18,9 +18,11 @@
|
|||||||
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
|
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
|
||||||
<ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
|
<ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
|
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
|
||||||
|
<ProjectReference Include="..\src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BitFaster.Caching" />
|
||||||
<PackageReference Include="DiscUtils.Udf" />
|
<PackageReference Include="DiscUtils.Udf" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" />
|
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -62,10 +64,15 @@
|
|||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Ignore" />
|
||||||
|
<PackageReference Include="StackExchange.Redis" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Localization\iso6392.txt" />
|
<EmbeddedResource Include="Localization\iso6392.txt" />
|
||||||
<EmbeddedResource Include="Localization\countries.json" />
|
<EmbeddedResource Include="Localization\countries.json" />
|
||||||
<EmbeddedResource Include="Localization\Core\*.json" />
|
<EmbeddedResource Include="Localization\Core\*.json" />
|
||||||
<EmbeddedResource Include="Localization\Ratings\*.csv" />
|
<EmbeddedResource Include="Localization\Ratings\*.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
|
||||||
using Jellyfin.Data.Events;
|
using Jellyfin.Data.Events;
|
||||||
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
@@ -34,7 +34,7 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
|
|||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly ILogger<LibraryChangedNotifier> _logger;
|
private readonly ILogger<LibraryChangedNotifier> _logger;
|
||||||
|
|
||||||
private readonly object _libraryChangedSyncLock = new();
|
private readonly Lock _libraryChangedSyncLock = new();
|
||||||
private readonly List<Folder> _foldersAddedTo = new();
|
private readonly List<Folder> _foldersAddedTo = new();
|
||||||
private readonly List<Folder> _foldersRemovedFrom = new();
|
private readonly List<Folder> _foldersRemovedFrom = new();
|
||||||
private readonly List<BaseItem> _itemsAdded = new();
|
private readonly List<BaseItem> _itemsAdded = new();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
|
|
||||||
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
|
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
|
||||||
private readonly object _syncLock = new();
|
private readonly Lock _syncLock = new();
|
||||||
|
|
||||||
private Timer? _updateTimer;
|
private Timer? _updateTimer;
|
||||||
|
|
||||||
@@ -144,9 +144,15 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
var dto = _userDataManager.GetUserDataDto(i, user);
|
var dto = _userDataManager.GetUserDataDto(i, user);
|
||||||
|
if (dto is null)
|
||||||
|
{
|
||||||
|
return null!;
|
||||||
|
}
|
||||||
|
|
||||||
dto.ItemId = i.Id;
|
dto.ItemId = i.Id;
|
||||||
return dto;
|
return dto;
|
||||||
})
|
})
|
||||||
|
.Where(e => e is not null)
|
||||||
.ToArray()
|
.ToArray()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
RemoteEndPoint = remoteEndPoint;
|
RemoteEndPoint = remoteEndPoint;
|
||||||
|
|
||||||
_jsonOptions = JsonDefaults.Options;
|
_jsonOptions = JsonDefaults.Options;
|
||||||
LastActivityDate = DateTime.Now;
|
LastActivityDate = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -82,17 +82,17 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
public WebSocketState State => _socket.State;
|
public WebSocketState State => _socket.State;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
|
public async Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
||||||
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
|
await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
|
public async Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
||||||
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
|
await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -224,12 +224,12 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SendKeepAliveResponse()
|
private async Task SendKeepAliveResponse()
|
||||||
{
|
{
|
||||||
LastKeepAliveDate = DateTime.UtcNow;
|
LastKeepAliveDate = DateTime.UtcNow;
|
||||||
return SendAsync(
|
await SendAsync(
|
||||||
new OutboundKeepAliveMessage(),
|
new OutboundKeepAliveMessage(),
|
||||||
CancellationToken.None);
|
CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
/// Processes the web socket message received.
|
/// Processes the web socket message received.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="result">The result.</param>
|
/// <param name="result">The result.</param>
|
||||||
private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
|
private async Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
|
||||||
{
|
{
|
||||||
var tasks = new Task[_webSocketListeners.Length];
|
var tasks = new Task[_webSocketListeners.Length];
|
||||||
for (var i = 0; i < _webSocketListeners.Length; ++i)
|
for (var i = 0; i < _webSocketListeners.Length; ++i)
|
||||||
@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
tasks[i] = _webSocketListeners[i].ProcessMessageAsync(result);
|
tasks[i] = _webSocketListeners[i].ProcessMessageAsync(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.WhenAll(tasks);
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ namespace Emby.Server.Implementations.IO
|
|||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
private readonly IServerConfigurationManager _configurationManager;
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
|
|
||||||
private readonly List<string> _affectedPaths = new List<string>();
|
private readonly List<string> _affectedPaths = new();
|
||||||
private readonly object _timerLock = new object();
|
private readonly Lock _timerLock = new();
|
||||||
private Timer? _timer;
|
private Timer? _timer;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
private void ProcessPathChanges(List<string> paths)
|
private void ProcessPathChanges(List<string> paths)
|
||||||
{
|
{
|
||||||
IEnumerable<BaseItem> itemsToRefresh = paths
|
IEnumerable<BaseItem> itemsToRefresh = paths
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct()
|
||||||
.Select(GetAffectedBaseItem)
|
.Select(GetAffectedBaseItem)
|
||||||
.Where(item => item is not null)
|
.Where(item => item is not null)
|
||||||
.DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where()
|
.DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where()
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
.Where(IsLibraryMonitorEnabled)
|
.Where(IsLibraryMonitorEnabled)
|
||||||
.OfType<Folder>()
|
.OfType<Folder>()
|
||||||
.SelectMany(f => f.PhysicalLocations)
|
.SelectMany(f => f.PhysicalLocations)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct()
|
||||||
.Order();
|
.Order();
|
||||||
|
|
||||||
foreach (var path in paths)
|
foreach (var path in paths)
|
||||||
@@ -352,6 +352,12 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||||
|
if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
|
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
|
||||||
foreach (var i in _tempIgnoredPaths.Keys)
|
foreach (var i in _tempIgnoredPaths.Keys)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using System.Security;
|
using System.Security;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -152,6 +153,10 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void MoveDirectory(string source, string destination)
|
public void MoveDirectory(string source, string destination)
|
||||||
{
|
{
|
||||||
|
// Make sure parent directory of target exists
|
||||||
|
var parent = Directory.GetParent(destination);
|
||||||
|
parent?.Create();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.Move(source, destination);
|
Directory.Move(source, destination);
|
||||||
@@ -160,12 +165,13 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
// Cross device move requires a copy
|
// Cross device move requires a copy
|
||||||
Directory.CreateDirectory(destination);
|
Directory.CreateDirectory(destination);
|
||||||
foreach (string file in Directory.GetFiles(source))
|
var sourceDir = new DirectoryInfo(source);
|
||||||
|
foreach (var file in sourceDir.EnumerateFiles())
|
||||||
{
|
{
|
||||||
File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), true);
|
file.CopyTo(Path.Combine(destination, file.Name), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Directory.Delete(source, true);
|
sourceDir.Delete(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,40 +253,40 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
|
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
|
||||||
|
|
||||||
// if (!result.IsDirectory)
|
|
||||||
// {
|
|
||||||
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (info is FileInfo fileInfo)
|
if (info is FileInfo fileInfo)
|
||||||
{
|
{
|
||||||
result.Length = fileInfo.Length;
|
result.CreationTimeUtc = GetCreationTimeUtc(info);
|
||||||
|
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
|
||||||
// Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
|
if (fileInfo.LinkTarget is not null)
|
||||||
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true);
|
||||||
|
if (targetFileInfo is not null)
|
||||||
{
|
{
|
||||||
result.Length = RandomAccess.GetLength(fileHandle);
|
result.Exists = targetFileInfo.Exists;
|
||||||
|
if (result.Exists)
|
||||||
|
{
|
||||||
|
result.Length = targetFileInfo.Length;
|
||||||
|
result.CreationTimeUtc = GetCreationTimeUtc(targetFileInfo);
|
||||||
|
result.LastWriteTimeUtc = GetLastWriteTimeUtc(targetFileInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Exists = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (FileNotFoundException ex)
|
|
||||||
{
|
|
||||||
// Dangling symlinks cannot be detected before opening the file unfortunately...
|
|
||||||
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
|
|
||||||
result.Exists = false;
|
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Length = fileInfo.Length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.CreationTimeUtc = GetCreationTimeUtc(info);
|
|
||||||
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -491,8 +497,17 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual bool AreEqual(string path1, string path2)
|
public virtual bool AreEqual(string path1, string path2)
|
||||||
{
|
{
|
||||||
return Path.TrimEndingDirectorySeparator(path1).Equals(
|
if (string.IsNullOrWhiteSpace(path1) || string.IsNullOrWhiteSpace(path2))
|
||||||
Path.TrimEndingDirectorySeparator(path2),
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized1 = Path.TrimEndingDirectorySeparator(path1);
|
||||||
|
var normalized2 = Path.TrimEndingDirectorySeparator(path2);
|
||||||
|
|
||||||
|
return string.Equals(
|
||||||
|
normalized1,
|
||||||
|
normalized2,
|
||||||
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,8 +549,8 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return DriveInfo.GetDrives()
|
return DriveInfo.GetDrives()
|
||||||
.Where(
|
.Where(
|
||||||
d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
|
d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
|
||||||
&& d.IsReady
|
&& d.IsReady
|
||||||
&& d.TotalSize != 0)
|
&& d.TotalSize != 0)
|
||||||
.Select(d => new FileSystemMetadata
|
.Select(d => new FileSystemMetadata
|
||||||
{
|
{
|
||||||
Name = d.Name,
|
Name = d.Name,
|
||||||
@@ -553,22 +568,36 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
return GetFiles(path, null, false, recursive);
|
return GetFiles(path, "*", recursive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, bool recursive = false)
|
||||||
|
{
|
||||||
|
return GetFiles(path, searchPattern, null, false, recursive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive)
|
||||||
|
{
|
||||||
|
return GetFiles(path, "*", extensions, enableCaseSensitiveExtensions, recursive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||||
{
|
{
|
||||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
// On linux and osx the search pattern is case sensitive
|
// On linux and macOS the search pattern is case-sensitive
|
||||||
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
||||||
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1)
|
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1)
|
||||||
{
|
{
|
||||||
return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
|
searchPattern = searchPattern.EndsWith(extensions[0], StringComparison.Ordinal) ? searchPattern : searchPattern + extensions[0];
|
||||||
|
|
||||||
|
return ToMetadata(new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
|
var files = new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions);
|
||||||
|
|
||||||
if (extensions is not null && extensions.Count > 0)
|
if (extensions is not null && extensions.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -590,6 +619,9 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
|
// Note: any of unhandled exceptions thrown by this method may cause the caller to believe the whole path is not accessible.
|
||||||
|
// But what causing the exception may be a single file under that path. This could lead to unexpected behavior.
|
||||||
|
// For example, the scanner will remove everything in that path due to unhandled errors.
|
||||||
var directoryInfo = new DirectoryInfo(path);
|
var directoryInfo = new DirectoryInfo(path);
|
||||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
@@ -618,7 +650,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
// On linux and osx the search pattern is case sensitive
|
// On linux and macOS the search pattern is case-sensitive
|
||||||
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
||||||
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Length == 1)
|
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Length == 1)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
@@ -42,13 +43,11 @@ namespace Emby.Server.Implementations.Images
|
|||||||
protected IImageProcessor ImageProcessor { get; set; }
|
protected IImageProcessor ImageProcessor { get; set; }
|
||||||
|
|
||||||
protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
|
protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
|
||||||
= new ImageType[] { ImageType.Primary };
|
= [ImageType.Primary];
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Name => "Dynamic Image Provider";
|
public string Name => "Dynamic Image Provider";
|
||||||
|
|
||||||
protected virtual int MaxImageAgeDays => 7;
|
|
||||||
|
|
||||||
public int Order => 0;
|
public int Order => 0;
|
||||||
|
|
||||||
protected virtual bool Supports(BaseItem item) => true;
|
protected virtual bool Supports(BaseItem item) => true;
|
||||||
@@ -116,13 +115,12 @@ namespace Emby.Server.Implementations.Images
|
|||||||
|
|
||||||
var mimeType = MimeTypes.GetMimeType(outputPath);
|
var mimeType = MimeTypes.GetMimeType(outputPath);
|
||||||
|
|
||||||
if (string.Equals(mimeType, "application/octet-stream", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(mimeType, MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
mimeType = "image/png";
|
mimeType = MediaTypeNames.Image.Png;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
|
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
|
||||||
File.Delete(outputPath);
|
|
||||||
|
|
||||||
return ItemUpdateType.ImageUpdate;
|
return ItemUpdateType.ImageUpdate;
|
||||||
}
|
}
|
||||||
@@ -269,22 +267,24 @@ namespace Emby.Server.Implementations.Images
|
|||||||
{
|
{
|
||||||
var image = item.GetImageInfo(type, 0);
|
var image = item.GetImageInfo(type, 0);
|
||||||
|
|
||||||
if (image is not null)
|
if (image is null)
|
||||||
{
|
{
|
||||||
if (!image.IsLocalFile)
|
return GetItemsWithImages(item).Count is not 0;
|
||||||
{
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
|
if (!image.IsLocalFile)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!HasChangedByDate(item, image))
|
if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!HasChangedByDate(item, image))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -292,8 +292,14 @@ namespace Emby.Server.Implementations.Images
|
|||||||
|
|
||||||
protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image)
|
protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image)
|
||||||
{
|
{
|
||||||
var age = DateTime.UtcNow - image.DateModified;
|
var path = image.Path;
|
||||||
return age.TotalDays > MaxImageAgeDays;
|
if (!string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
var modificationDate = FileSystem.GetLastWriteTimeUtc(path);
|
||||||
|
return image.DateModified != modificationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)
|
protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
@@ -97,5 +98,11 @@ namespace Emby.Server.Implementations.Images
|
|||||||
|
|
||||||
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
|
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
|
||||||
|
{
|
||||||
|
var age = DateTime.UtcNow - image.DateModified;
|
||||||
|
return age.TotalDays > 7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
|
|||||||
@@ -37,46 +37,33 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't ignore top level folders
|
|
||||||
if (fileInfo.IsDirectory && parent is AggregateFolder)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IgnorePatterns.ShouldIgnore(fileInfo.FullName))
|
if (IgnorePatterns.ShouldIgnore(fileInfo.FullName))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var filename = fileInfo.Name;
|
// Don't ignore top level folders
|
||||||
|
if (fileInfo.IsDirectory
|
||||||
|
&& (parent is AggregateFolder || (parent?.IsTopParent ?? false)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (fileInfo.IsDirectory)
|
if (fileInfo.IsDirectory)
|
||||||
{
|
{
|
||||||
if (parent is not null)
|
// Ignore extras for unsupported types
|
||||||
{
|
return _namingOptions.AllExtrasTypesFolderNames.ContainsKey(fileInfo.Name)
|
||||||
// Ignore extras folders but allow it at the collection level
|
&& parent is not UserRootFolder;
|
||||||
if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
|
|
||||||
&& parent is not AggregateFolder
|
|
||||||
&& parent is not UserRootFolder)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (parent is not null)
|
|
||||||
{
|
|
||||||
// Don't resolve these into audio files
|
|
||||||
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
|
|
||||||
&& AudioFileParser.IsAudioFile(filename, _namingOptions))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
// Don't resolve theme songs
|
||||||
|
return Path.GetFileNameWithoutExtension(fileInfo.Name.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
|
||||||
|
&& AudioFileParser.IsAudioFile(fileInfo.Name, _namingOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
|
using MediaBrowser.Controller.Resolvers;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Library;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolver rule class for ignoring files via .ignore.
|
||||||
|
/// </summary>
|
||||||
|
public class DotIgnoreIgnoreRule : IResolverIgnoreRule
|
||||||
|
{
|
||||||
|
private static readonly bool IsWindows = OperatingSystem.IsWindows();
|
||||||
|
|
||||||
|
private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
|
||||||
|
{
|
||||||
|
for (var current = directory; current is not null; current = current.Parent)
|
||||||
|
{
|
||||||
|
var ignorePath = Path.Join(current.FullName, ".ignore");
|
||||||
|
if (File.Exists(ignorePath))
|
||||||
|
{
|
||||||
|
return new FileInfo(ignorePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether or not the file is ignored.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileInfo">The file information.</param>
|
||||||
|
/// <param name="parent">The parent BaseItem.</param>
|
||||||
|
/// <returns>True if the file should be ignored.</returns>
|
||||||
|
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
|
||||||
|
{
|
||||||
|
var searchDirectory = fileInfo.IsDirectory
|
||||||
|
? new DirectoryInfo(fileInfo.FullName)
|
||||||
|
: new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(searchDirectory.FullName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ignoreFile = FindIgnoreFile(searchDirectory);
|
||||||
|
if (ignoreFile is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast path in case the ignore files isn't a symlink and is empty
|
||||||
|
if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
|
||||||
|
{
|
||||||
|
// Ignore directory if we just have the file
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = GetFileContent(ignoreFile);
|
||||||
|
return string.IsNullOrWhiteSpace(content)
|
||||||
|
|| CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
|
||||||
|
{
|
||||||
|
// If file has content, base ignoring off the content .gitignore-style rules
|
||||||
|
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
return CheckIgnoreRules(path, rules, isDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a path should be ignored based on an array of ignore rules.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path to check.</param>
|
||||||
|
/// <param name="rules">The array of ignore rules.</param>
|
||||||
|
/// <param name="isDirectory">Whether the path is a directory.</param>
|
||||||
|
/// <returns>True if the path should be ignored.</returns>
|
||||||
|
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory)
|
||||||
|
=> CheckIgnoreRules(path, rules, isDirectory, IsWindows);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a path should be ignored based on an array of ignore rules.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path to check.</param>
|
||||||
|
/// <param name="rules">The array of ignore rules.</param>
|
||||||
|
/// <param name="isDirectory">Whether the path is a directory.</param>
|
||||||
|
/// <param name="normalizePath">Whether to normalize backslashes to forward slashes (for Windows paths).</param>
|
||||||
|
/// <returns>True if the path should be ignored.</returns>
|
||||||
|
internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath)
|
||||||
|
{
|
||||||
|
var ignore = new Ignore.Ignore();
|
||||||
|
|
||||||
|
// Add each rule individually to catch and skip invalid patterns
|
||||||
|
var validRulesAdded = 0;
|
||||||
|
foreach (var rule in rules)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ignore.Add(rule);
|
||||||
|
validRulesAdded++;
|
||||||
|
}
|
||||||
|
catch (RegexParseException)
|
||||||
|
{
|
||||||
|
// Ignore invalid patterns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no valid rules were added, fall back to ignoring everything (like an empty .ignore file)
|
||||||
|
if (validRulesAdded == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
|
||||||
|
// See https://github.com/jellyfin/jellyfin/issues/15484
|
||||||
|
var pathToCheck = normalizePath ? path.NormalizePath('/') : path;
|
||||||
|
|
||||||
|
// Add trailing slash for directories to match "folder/"
|
||||||
|
if (isDirectory)
|
||||||
|
{
|
||||||
|
pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ignore.IsIgnored(pathToCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFileContent(FileInfo ignoreFile)
|
||||||
|
{
|
||||||
|
ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
|
||||||
|
return ignoreFile.Exists
|
||||||
|
? File.ReadAllText(ignoreFile.FullName)
|
||||||
|
: string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Controller.Chapters;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
|
using MediaBrowser.Controller.MediaSegments;
|
||||||
|
using MediaBrowser.Controller.Trickplay;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Library;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IExternalDataManager implementation.
|
||||||
|
/// </summary>
|
||||||
|
public class ExternalDataManager : IExternalDataManager
|
||||||
|
{
|
||||||
|
private readonly IKeyframeManager _keyframeManager;
|
||||||
|
private readonly IMediaSegmentManager _mediaSegmentManager;
|
||||||
|
private readonly IPathManager _pathManager;
|
||||||
|
private readonly ITrickplayManager _trickplayManager;
|
||||||
|
private readonly IChapterManager _chapterManager;
|
||||||
|
private readonly ILogger<ExternalDataManager> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ExternalDataManager"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyframeManager">The keyframe manager.</param>
|
||||||
|
/// <param name="mediaSegmentManager">The media segment manager.</param>
|
||||||
|
/// <param name="pathManager">The path manager.</param>
|
||||||
|
/// <param name="trickplayManager">The trickplay manager.</param>
|
||||||
|
/// <param name="chapterManager">The chapter manager.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
public ExternalDataManager(
|
||||||
|
IKeyframeManager keyframeManager,
|
||||||
|
IMediaSegmentManager mediaSegmentManager,
|
||||||
|
IPathManager pathManager,
|
||||||
|
ITrickplayManager trickplayManager,
|
||||||
|
IChapterManager chapterManager,
|
||||||
|
ILogger<ExternalDataManager> logger)
|
||||||
|
{
|
||||||
|
_keyframeManager = keyframeManager;
|
||||||
|
_mediaSegmentManager = mediaSegmentManager;
|
||||||
|
_pathManager = pathManager;
|
||||||
|
_trickplayManager = trickplayManager;
|
||||||
|
_chapterManager = chapterManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var validPaths = _pathManager.GetExtractedDataPaths(item).Where(Directory.Exists).ToList();
|
||||||
|
var itemId = item.Id;
|
||||||
|
if (validPaths.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var path in validPaths)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(path, true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Unable to prune external item data at {Path}: {Exception}", path, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||||
|
await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||||
|
await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||||
|
await _chapterManager.DeleteChapterDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using DotNet.Globbing;
|
using DotNet.Globbing;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library
|
namespace Emby.Server.Implementations.Library
|
||||||
@@ -49,6 +48,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
"**/.wd_tv",
|
"**/.wd_tv",
|
||||||
"**/lost+found/**",
|
"**/lost+found/**",
|
||||||
"**/lost+found",
|
"**/lost+found",
|
||||||
|
"**/subs/**",
|
||||||
|
"**/subs",
|
||||||
|
"**/.snapshots/**",
|
||||||
|
"**/.snapshots",
|
||||||
|
"**/.snapshot/**",
|
||||||
|
"**/.snapshot",
|
||||||
|
|
||||||
// Trickplay files
|
// Trickplay files
|
||||||
"**/*.trickplay",
|
"**/*.trickplay",
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.MediaEncoding.Keyframes;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
|
using MediaBrowser.Controller.Persistence;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Library;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manager for Keyframe data.
|
||||||
|
/// </summary>
|
||||||
|
public class KeyframeManager : IKeyframeManager
|
||||||
|
{
|
||||||
|
private readonly IKeyframeRepository _repository;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="KeyframeManager"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository">The keyframe repository.</param>
|
||||||
|
public KeyframeManager(IKeyframeRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId)
|
||||||
|
{
|
||||||
|
return _repository.GetKeyframeData(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -12,8 +13,10 @@ using System.Text.Json;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AsyncKeyedLock;
|
using AsyncKeyedLock;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
@@ -38,7 +41,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
public class MediaSourceManager : IMediaSourceManager, IDisposable
|
public class MediaSourceManager : IMediaSourceManager, IDisposable
|
||||||
{
|
{
|
||||||
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
|
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
|
||||||
private const char LiveStreamIdDelimeter = '_';
|
private const char LiveStreamIdDelimiter = '_';
|
||||||
|
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
private readonly IItemRepository _itemRepo;
|
private readonly IItemRepository _itemRepo;
|
||||||
@@ -51,7 +54,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
private readonly ILocalizationManager _localizationManager;
|
private readonly ILocalizationManager _localizationManager;
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly IMediaStreamRepository _mediaStreamRepository;
|
||||||
|
private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
|
||||||
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
|
private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
|
||||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||||
@@ -69,7 +73,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IUserDataManager userDataManager,
|
IUserDataManager userDataManager,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IDirectoryService directoryService)
|
IDirectoryService directoryService,
|
||||||
|
IMediaStreamRepository mediaStreamRepository,
|
||||||
|
IMediaAttachmentRepository mediaAttachmentRepository)
|
||||||
{
|
{
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
_itemRepo = itemRepo;
|
_itemRepo = itemRepo;
|
||||||
@@ -82,6 +88,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_localizationManager = localizationManager;
|
_localizationManager = localizationManager;
|
||||||
_appPaths = applicationPaths;
|
_appPaths = applicationPaths;
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
|
_mediaStreamRepository = mediaStreamRepository;
|
||||||
|
_mediaAttachmentRepository = mediaAttachmentRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
|
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
|
||||||
@@ -89,9 +97,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_providers = providers.ToArray();
|
_providers = providers.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
|
public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query)
|
||||||
{
|
{
|
||||||
var list = _itemRepo.GetMediaStreams(query);
|
var list = _mediaStreamRepository.GetMediaStreams(query);
|
||||||
|
|
||||||
foreach (var stream in list)
|
foreach (var stream in list)
|
||||||
{
|
{
|
||||||
@@ -121,7 +129,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MediaStream> GetMediaStreams(Guid itemId)
|
public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId)
|
||||||
{
|
{
|
||||||
var list = GetMediaStreams(new MediaStreamQuery
|
var list = GetMediaStreams(new MediaStreamQuery
|
||||||
{
|
{
|
||||||
@@ -131,7 +139,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return GetMediaStreamsForItem(list);
|
return GetMediaStreamsForItem(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<MediaStream> GetMediaStreamsForItem(List<MediaStream> streams)
|
private IReadOnlyList<MediaStream> GetMediaStreamsForItem(IReadOnlyList<MediaStream> streams)
|
||||||
{
|
{
|
||||||
foreach (var stream in streams)
|
foreach (var stream in streams)
|
||||||
{
|
{
|
||||||
@@ -145,13 +153,13 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
|
public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
|
||||||
{
|
{
|
||||||
return _itemRepo.GetMediaAttachments(query);
|
return _mediaAttachmentRepository.GetMediaAttachments(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public List<MediaAttachment> GetMediaAttachments(Guid itemId)
|
public IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId)
|
||||||
{
|
{
|
||||||
return GetMediaAttachments(new MediaAttachmentQuery
|
return GetMediaAttachments(new MediaAttachmentQuery
|
||||||
{
|
{
|
||||||
@@ -159,7 +167,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
|
public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
|
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
|
||||||
|
|
||||||
@@ -212,12 +220,17 @@ namespace Emby.Server.Implementations.Library
|
|||||||
list.Add(source);
|
list.Add(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SortMediaSources(list);
|
return SortMediaSources(list).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />>
|
/// <inheritdoc />>
|
||||||
public MediaProtocol GetPathProtocol(string path)
|
public MediaProtocol GetPathProtocol(string path)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
return MediaProtocol.File;
|
||||||
|
}
|
||||||
|
|
||||||
if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
|
if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return MediaProtocol.Rtsp;
|
return MediaProtocol.Rtsp;
|
||||||
@@ -307,7 +320,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
|
private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
|
||||||
{
|
{
|
||||||
var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimeter;
|
var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -332,7 +345,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
|
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
|
public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(item);
|
ArgumentNullException.ThrowIfNull(item);
|
||||||
|
|
||||||
@@ -371,7 +384,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var culture = _localizationManager.FindLanguageInfo(language);
|
var culture = _localizationManager.FindLanguageInfo(language);
|
||||||
if (culture is not null)
|
if (culture is not null)
|
||||||
{
|
{
|
||||||
return culture.ThreeLetterISOLanguageNames;
|
return culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase) ? [culture.Name] : culture.ThreeLetterISOLanguageNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [language];
|
return [language];
|
||||||
@@ -419,6 +432,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
|
if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
|
||||||
{
|
{
|
||||||
source.DefaultAudioStreamIndex = index;
|
source.DefaultAudioStreamIndex = index;
|
||||||
|
source.DefaultAudioIndexSource = AudioIndexSource.User;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,6 +440,15 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
|
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
|
||||||
|
|
||||||
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
|
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
|
||||||
|
if (user.PlayDefaultAudioTrack)
|
||||||
|
{
|
||||||
|
source.DefaultAudioIndexSource |= AudioIndexSource.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferredAudio.Count > 0)
|
||||||
|
{
|
||||||
|
source.DefaultAudioIndexSource |= AudioIndexSource.Language;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
|
public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
|
||||||
@@ -453,7 +476,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
|
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
|
||||||
{
|
{
|
||||||
return sources.OrderBy(i =>
|
return sources.OrderBy(i =>
|
||||||
{
|
{
|
||||||
@@ -470,8 +493,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
return stream?.Width ?? 0;
|
return stream?.Width ?? 0;
|
||||||
})
|
})
|
||||||
.Where(i => i.Type != MediaSourceType.Placeholder)
|
.Where(i => i.Type != MediaSourceType.Placeholder);
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
|
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
|
||||||
@@ -640,7 +662,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
|
_logger.LogDebug(ex, "Error parsing cached media info.");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -664,17 +686,17 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
mediaInfo = await _mediaEncoder.GetMediaInfo(
|
mediaInfo = await _mediaEncoder.GetMediaInfo(
|
||||||
new MediaInfoRequest
|
new MediaInfoRequest
|
||||||
{
|
{
|
||||||
MediaSource = mediaSource,
|
MediaSource = mediaSource,
|
||||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||||
ExtractChapters = false
|
ExtractChapters = false
|
||||||
},
|
},
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (cacheFilePath is not null)
|
if (cacheFilePath is not null)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
||||||
FileStream createStream = File.Create(cacheFilePath);
|
FileStream createStream = AsyncFile.Create(cacheFilePath);
|
||||||
await using (createStream.ConfigureAwait(false))
|
await using (createStream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -777,9 +799,13 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrEmpty(id);
|
ArgumentException.ThrowIfNullOrEmpty(id);
|
||||||
|
|
||||||
// TODO probably shouldn't throw here but it is kept for "backwards compatibility"
|
var info = GetLiveStreamInfo(id);
|
||||||
var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
|
if (info is null)
|
||||||
return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
|
{
|
||||||
|
return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ILiveStream GetLiveStreamInfo(string id)
|
public ILiveStream GetLiveStreamInfo(string id)
|
||||||
@@ -806,7 +832,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return result.Item1;
|
return result.Item1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
|
public async Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var stream = new MediaSourceInfo
|
var stream = new MediaSourceInfo
|
||||||
{
|
{
|
||||||
@@ -829,10 +855,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
|
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
|
||||||
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
|
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return new List<MediaSourceInfo>
|
return [stream];
|
||||||
{
|
|
||||||
stream
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CloseLiveStream(string id)
|
public async Task CloseLiveStream(string id)
|
||||||
@@ -864,11 +887,11 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||||
|
|
||||||
var keys = key.Split(LiveStreamIdDelimeter, 2);
|
var keys = key.Split(LiveStreamIdDelimiter, 2);
|
||||||
|
|
||||||
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
|
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
|
var splitIndex = key.IndexOf(LiveStreamIdDelimiter, StringComparison.Ordinal);
|
||||||
var keyId = key.Substring(splitIndex + 1);
|
var keyId = key.Substring(splitIndex + 1);
|
||||||
|
|
||||||
return (provider, keyId);
|
return (provider, keyId);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
@@ -39,46 +39,48 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort in the following order: Default > No tag > Forced
|
||||||
var sortedStreams = streams
|
var sortedStreams = streams
|
||||||
.Where(i => i.Type == MediaStreamType.Subtitle)
|
.Where(i => i.Type == MediaStreamType.Subtitle)
|
||||||
.OrderByDescending(x => x.IsExternal)
|
.OrderByDescending(x => x.IsExternal)
|
||||||
.ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ThenByDescending(x => x.IsForced)
|
|
||||||
.ThenByDescending(x => x.IsDefault)
|
.ThenByDescending(x => x.IsDefault)
|
||||||
.ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase))
|
.ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
|
||||||
|
.ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
|
||||||
|
.ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language))
|
||||||
|
.ThenByDescending(x => x.IsForced)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
MediaStream? stream = null;
|
MediaStream? stream = null;
|
||||||
|
|
||||||
if (mode == SubtitlePlaybackMode.Default)
|
if (mode == SubtitlePlaybackMode.Default)
|
||||||
{
|
{
|
||||||
// Load subtitles according to external, forced and default flags.
|
// Load subtitles according to external, default and forced flags.
|
||||||
stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
|
stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced);
|
||||||
}
|
}
|
||||||
else if (mode == SubtitlePlaybackMode.Smart)
|
else if (mode == SubtitlePlaybackMode.Smart)
|
||||||
{
|
{
|
||||||
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
|
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
|
||||||
// If no subtitles of preferred language available, use default behaviour.
|
// If no subtitles of preferred language available, use none.
|
||||||
|
// If the audio language is one of the user's preferred subtitle languages behave like OnlyForced.
|
||||||
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
|
stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages));
|
||||||
sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Respect forced flag.
|
stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
|
||||||
stream = sortedStreams.FirstOrDefault(x => x.IsForced);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (mode == SubtitlePlaybackMode.Always)
|
else if (mode == SubtitlePlaybackMode.Always)
|
||||||
{
|
{
|
||||||
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour.
|
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour.
|
||||||
stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
|
stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ??
|
||||||
sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
|
BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
|
||||||
}
|
}
|
||||||
else if (mode == SubtitlePlaybackMode.OnlyForced)
|
else if (mode == SubtitlePlaybackMode.OnlyForced)
|
||||||
{
|
{
|
||||||
// Only load subtitles that are flagged forced.
|
// Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
|
||||||
stream = sortedStreams.FirstOrDefault(x => x.IsForced);
|
stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
return stream?.Index;
|
return stream?.Index;
|
||||||
@@ -110,40 +112,72 @@ namespace Emby.Server.Implementations.Library
|
|||||||
if (mode == SubtitlePlaybackMode.Default)
|
if (mode == SubtitlePlaybackMode.Default)
|
||||||
{
|
{
|
||||||
// Prefer embedded metadata over smart logic
|
// Prefer embedded metadata over smart logic
|
||||||
filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault)
|
// Load subtitles according to external, default, and forced flags.
|
||||||
|
filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
else if (mode == SubtitlePlaybackMode.Smart)
|
else if (mode == SubtitlePlaybackMode.Smart)
|
||||||
{
|
{
|
||||||
// Prefer smart logic over embedded metadata
|
// Prefer smart logic over embedded metadata
|
||||||
|
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior.
|
||||||
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
|
filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (mode == SubtitlePlaybackMode.Always)
|
else if (mode == SubtitlePlaybackMode.Always)
|
||||||
{
|
{
|
||||||
// Always load the most suitable full subtitles
|
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior.
|
||||||
filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
|
filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages))
|
||||||
|
.ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages);
|
||||||
}
|
}
|
||||||
else if (mode == SubtitlePlaybackMode.OnlyForced)
|
else if (mode == SubtitlePlaybackMode.OnlyForced)
|
||||||
{
|
{
|
||||||
// Always load the most suitable full subtitles
|
// Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
|
||||||
filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
|
filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load forced subs if we have found no suitable full subtitles
|
// If filteredStreams is null, initialize it as an empty list to avoid null reference errors
|
||||||
var iterStreams = filteredStreams is null || filteredStreams.Count == 0
|
filteredStreams ??= new List<MediaStream>();
|
||||||
? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
|
|
||||||
: filteredStreams;
|
|
||||||
|
|
||||||
foreach (var stream in iterStreams)
|
foreach (var stream in filteredStreams)
|
||||||
{
|
{
|
||||||
stream.Score = GetStreamScore(stream, preferredLanguages);
|
stream.Score = GetStreamScore(stream, preferredLanguages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool MatchesPreferredLanguage(string language, IReadOnlyList<string> preferredLanguages)
|
||||||
|
{
|
||||||
|
// If preferredLanguages is empty, treat it as "any language" (wildcard)
|
||||||
|
return preferredLanguages.Count == 0 ||
|
||||||
|
preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsLanguageUndefined(string language)
|
||||||
|
{
|
||||||
|
// Check for null, empty, or known placeholders
|
||||||
|
return string.IsNullOrEmpty(language) ||
|
||||||
|
language.Equals("und", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
language.Equals("unknown", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
language.Equals("mul", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
language.Equals("zxx", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<MediaStream> BehaviorOnlyForced(IEnumerable<MediaStream> sortedStreams, IReadOnlyList<string> preferredLanguages)
|
||||||
|
{
|
||||||
|
return sortedStreams
|
||||||
|
.Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language)))
|
||||||
|
.OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
|
||||||
|
.ThenByDescending(s => IsLanguageUndefined(s.Language))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
|
internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
|
||||||
{
|
{
|
||||||
var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));
|
var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Data.Entities;
|
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -24,37 +26,35 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
|
public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
var list = new List<BaseItem>
|
var instantMixItems = GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||||
{
|
|
||||||
item
|
|
||||||
};
|
|
||||||
|
|
||||||
list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions));
|
return [item, .. instantMixItems.Where(i => !i.Id.Equals(item.Id))];
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
|
public IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
|
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
|
public IReadOnlyList<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
|
public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
var genres = item
|
var genres = item
|
||||||
.GetRecursiveChildren(user, new InternalItemsQuery(user)
|
.GetRecursiveChildren(
|
||||||
{
|
user,
|
||||||
IncludeItemTypes = [BaseItemKind.Audio],
|
new InternalItemsQuery(user)
|
||||||
DtoOptions = dtoOptions
|
{
|
||||||
})
|
IncludeItemTypes = [BaseItemKind.Audio],
|
||||||
|
DtoOptions = dtoOptions
|
||||||
|
},
|
||||||
|
out _)
|
||||||
.Cast<Audio>()
|
.Cast<Audio>()
|
||||||
.SelectMany(i => i.Genres)
|
.SelectMany(i => i.Genres)
|
||||||
.Concat(item.Genres)
|
.Concat(item.Genres)
|
||||||
@@ -63,12 +63,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return GetInstantMixFromGenres(genres, user, dtoOptions);
|
return GetInstantMixFromGenres(genres, user, dtoOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
|
public IReadOnlyList<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
|
public IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
var genreIds = genres.DistinctNames().Select(i =>
|
var genreIds = genres.DistinctNames().Select(i =>
|
||||||
{
|
{
|
||||||
@@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
|
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
|
public IReadOnlyList<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
return _libraryManager.GetItemList(new InternalItemsQuery(user)
|
return _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
@@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
|
public IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
if (item is MusicGenre)
|
if (item is MusicGenre)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Library;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IPathManager implementation.
|
||||||
|
/// </summary>
|
||||||
|
public class PathManager : IPathManager
|
||||||
|
{
|
||||||
|
private readonly IServerConfigurationManager _config;
|
||||||
|
private readonly IApplicationPaths _appPaths;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="PathManager"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config">The server configuration manager.</param>
|
||||||
|
/// <param name="appPaths">The application paths.</param>
|
||||||
|
public PathManager(
|
||||||
|
IServerConfigurationManager config,
|
||||||
|
IApplicationPaths appPaths)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_appPaths = appPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
|
||||||
|
|
||||||
|
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetAttachmentPath(string mediaSourceId, string fileName)
|
||||||
|
{
|
||||||
|
return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetAttachmentFolderPath(string mediaSourceId)
|
||||||
|
{
|
||||||
|
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
|
||||||
|
|
||||||
|
return Path.Join(AttachmentCachePath, id[..2], id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetSubtitleFolderPath(string mediaSourceId)
|
||||||
|
{
|
||||||
|
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
|
||||||
|
|
||||||
|
return Path.Join(SubtitleCachePath, id[..2], id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
|
||||||
|
{
|
||||||
|
return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
|
||||||
|
{
|
||||||
|
var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan();
|
||||||
|
|
||||||
|
return saveWithMedia
|
||||||
|
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(Path.GetFileName(item.Path), ".trickplay"))
|
||||||
|
: Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string GetChapterImageFolderPath(BaseItem item)
|
||||||
|
{
|
||||||
|
return Path.Combine(item.GetInternalMetadataPath(), "chapters");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string GetChapterImagePath(BaseItem item, long chapterPositionTicks)
|
||||||
|
{
|
||||||
|
var filename = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg";
|
||||||
|
|
||||||
|
return Path.Combine(GetChapterImageFolderPath(item), filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
|
||||||
|
{
|
||||||
|
var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||||
|
return [
|
||||||
|
GetAttachmentFolderPath(mediaSourceId),
|
||||||
|
GetSubtitleFolderPath(mediaSourceId),
|
||||||
|
GetTrickplayDirectory(item, false),
|
||||||
|
GetTrickplayDirectory(item, true),
|
||||||
|
GetChapterImageFolderPath(item)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,23 +136,33 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
if (config.UseFileCreationTimeForDateAdded)
|
if (config.UseFileCreationTimeForDateAdded)
|
||||||
{
|
{
|
||||||
// directoryService.getFile may return null
|
var fileCreationDate = info?.CreationTimeUtc;
|
||||||
if (info is not null)
|
if (fileCreationDate is not null)
|
||||||
{
|
{
|
||||||
var dateCreated = info.CreationTimeUtc;
|
var dateCreated = fileCreationDate;
|
||||||
|
if (dateCreated == DateTime.MinValue)
|
||||||
if (dateCreated.Equals(DateTime.MinValue))
|
|
||||||
{
|
{
|
||||||
dateCreated = DateTime.UtcNow;
|
dateCreated = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.DateCreated = dateCreated;
|
item.DateCreated = dateCreated.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
item.DateCreated = DateTime.UtcNow;
|
item.DateCreated = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (info is not null && !info.IsDirectory)
|
||||||
|
{
|
||||||
|
item.Size = info.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileModificationDate = info?.LastWriteTimeUtc;
|
||||||
|
if (fileModificationDate.HasValue)
|
||||||
|
{
|
||||||
|
item.DateModified = fileModificationDate.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
_ => _videoResolvers
|
_ => _videoResolvers
|
||||||
};
|
};
|
||||||
|
|
||||||
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType)
|
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
|
||||||
{
|
{
|
||||||
var extraResult = GetExtraInfo(path, _namingOptions);
|
var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
|
||||||
if (extraResult.ExtraType is null)
|
if (extraResult.ExtraType is null)
|
||||||
{
|
{
|
||||||
extraType = null;
|
extraType = null;
|
||||||
|
|||||||
@@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
}
|
}
|
||||||
|
|
||||||
var videoInfos = files
|
var videoInfos = files
|
||||||
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
|
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath))
|
||||||
.Where(f => f is not null)
|
.Where(f => f is not null)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
|
var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
|
||||||
|
|
||||||
var result = new MultiItemResolverResult
|
var result = new MultiItemResolverResult
|
||||||
{
|
{
|
||||||
@@ -369,13 +369,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
// We need to only look at the name of this actual item (not parents)
|
// We need to only look at the name of this actual item (not parents)
|
||||||
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
|
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
|
||||||
|
|
||||||
if (!justName.IsEmpty)
|
var tmdbid = justName.GetAttributeValue("tmdbid");
|
||||||
|
|
||||||
|
// If not in a mixed folder and ID not found in folder path, check filename
|
||||||
|
if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder)
|
||||||
{
|
{
|
||||||
// Check for TMDb id
|
tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid");
|
||||||
var tmdbid = justName.GetAttributeValue("tmdbid");
|
|
||||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(item.Path))
|
if (!string.IsNullOrEmpty(item.Path))
|
||||||
{
|
{
|
||||||
// Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)
|
// Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)
|
||||||
@@ -405,6 +408,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
|
|
||||||
if (child.IsDirectory)
|
if (child.IsDirectory)
|
||||||
{
|
{
|
||||||
|
if (NamingOptions.AllExtrasTypesFolderNames.ContainsKey(filename))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (IsDvdDirectory(child.FullName, filename, directoryService))
|
if (IsDvdDirectory(child.FullName, filename, directoryService))
|
||||||
{
|
{
|
||||||
var movie = new T
|
var movie = new T
|
||||||
@@ -456,12 +464,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
{
|
{
|
||||||
var videoPath = result.Items[0].Path;
|
var videoPath = result.Items[0].Path;
|
||||||
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
|
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
|
||||||
|
var hasOtherSubfolders = multiDiscFolders.Count > 0;
|
||||||
|
|
||||||
if (!hasPhotos)
|
if (!hasPhotos && !hasOtherSubfolders)
|
||||||
{
|
{
|
||||||
var movie = (T)result.Items[0];
|
var movie = (T)result.Items[0];
|
||||||
movie.IsInMixedFolder = false;
|
movie.IsInMixedFolder = false;
|
||||||
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
|
if (collectionType == CollectionType.movies || collectionType is null)
|
||||||
|
{
|
||||||
|
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
|
||||||
|
}
|
||||||
|
|
||||||
return movie;
|
return movie;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
{
|
{
|
||||||
if (args.IsDirectory)
|
if (args.IsDirectory)
|
||||||
{
|
{
|
||||||
// It's a boxset if the path is a directory with [playlist] in its name
|
// It's a playlist if the path is a directory with [playlist] in its name
|
||||||
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
|
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
|
||||||
if (string.IsNullOrEmpty(filename))
|
if (string.IsNullOrEmpty(filename))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
|
|
||||||
var path = args.Path;
|
var path = args.Path;
|
||||||
|
|
||||||
var seasonParserResult = SeasonPathParser.Parse(path, true, true);
|
var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true);
|
||||||
|
|
||||||
var season = new Season
|
var season = new Season
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
{
|
{
|
||||||
if (child.IsDirectory)
|
if (child.IsDirectory)
|
||||||
{
|
{
|
||||||
if (IsSeasonFolder(child.FullName, isTvContentType))
|
if (IsSeasonFolder(child.FullName, path, isTvContentType))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
|
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
|
||||||
return true;
|
return true;
|
||||||
@@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
/// Determines whether [is season folder] [the specified path].
|
/// Determines whether [is season folder] [the specified path].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
|
/// <param name="parentPath">The parentpath.</param>
|
||||||
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
|
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
|
||||||
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
|
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
|
||||||
private static bool IsSeasonFolder(string path, bool isTvContentType)
|
private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType)
|
||||||
{
|
{
|
||||||
var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
|
var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber;
|
||||||
|
|
||||||
return seasonNumber.HasValue;
|
return seasonNumber.HasValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Data.Entities;
|
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -171,7 +172,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
List<BaseItem> mediaItems;
|
IReadOnlyList<BaseItem> mediaItems;
|
||||||
|
|
||||||
if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
|
if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library;
|
namespace Emby.Server.Implementations.Library;
|
||||||
@@ -43,14 +43,26 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList();
|
var posters = GetItemsWithImageType(ImageType.Primary)
|
||||||
var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList();
|
.Select(x => x.GetImages(ImageType.Primary).FirstOrDefault()?.Path)
|
||||||
|
.Where(path => !string.IsNullOrEmpty(path))
|
||||||
|
.Select(path => path!)
|
||||||
|
.ToList();
|
||||||
|
var backdrops = GetItemsWithImageType(ImageType.Thumb)
|
||||||
|
.Select(x => x.GetImages(ImageType.Thumb).FirstOrDefault()?.Path)
|
||||||
|
.Where(path => !string.IsNullOrEmpty(path))
|
||||||
|
.Select(path => path!)
|
||||||
|
.ToList();
|
||||||
if (backdrops.Count == 0)
|
if (backdrops.Count == 0)
|
||||||
{
|
{
|
||||||
// Thumb images fit better because they include the title in the image but are not provided with TMDb.
|
// Thumb images fit better because they include the title in the image but are not provided with TMDb.
|
||||||
// Using backdrops as a fallback to generate an image at all
|
// Using backdrops as a fallback to generate an image at all
|
||||||
_logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen");
|
_logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen");
|
||||||
backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList();
|
backdrops = GetItemsWithImageType(ImageType.Backdrop)
|
||||||
|
.Select(x => x.GetImages(ImageType.Backdrop).FirstOrDefault()?.Path)
|
||||||
|
.Where(path => !string.IsNullOrEmpty(path))
|
||||||
|
.Select(path => path!)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
_imageEncoder.CreateSplashscreen(posters, backdrops);
|
_imageEncoder.CreateSplashscreen(posters, backdrops);
|
||||||
@@ -65,15 +77,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
|
|||||||
CollapseBoxSetItems = false,
|
CollapseBoxSetItems = false,
|
||||||
Recursive = true,
|
Recursive = true,
|
||||||
DtoOptions = new DtoOptions(false),
|
DtoOptions = new DtoOptions(false),
|
||||||
ImageTypes = new[] { imageType },
|
ImageTypes = [imageType],
|
||||||
Limit = 30,
|
Limit = 30,
|
||||||
// TODO max parental rating configurable
|
// TODO max parental rating configurable
|
||||||
MaxParentalRating = 10,
|
MaxParentalRating = new(10, null),
|
||||||
OrderBy = new[]
|
OrderBy =
|
||||||
{
|
[
|
||||||
(ItemSortBy.Random, SortOrder.Ascending)
|
(ItemSortBy.Random, SortOrder.Ascending)
|
||||||
},
|
],
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series }
|
IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
|
#pragma warning disable RS0030 // Do not use banned APIs
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Jellyfin.Data.Entities;
|
using BitFaster.Caching.Lru;
|
||||||
|
using Jellyfin.Database.Implementations;
|
||||||
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Persistence;
|
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
|
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
|
||||||
using Book = MediaBrowser.Controller.Entities.Book;
|
using Book = MediaBrowser.Controller.Entities.Book;
|
||||||
|
|
||||||
@@ -22,27 +25,22 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class UserDataManager : IUserDataManager
|
public class UserDataManager : IUserDataManager
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, UserItemData> _userData =
|
|
||||||
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IDbContextFactory<JellyfinDbContext> _repository;
|
||||||
private readonly IUserDataRepository _repository;
|
private readonly FastConcurrentLru<string, UserItemData> _cache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UserDataManager"/> class.
|
/// Initializes a new instance of the <see cref="UserDataManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="repository">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
|
||||||
/// <param name="repository">Instance of the <see cref="IUserDataRepository"/> interface.</param>
|
|
||||||
public UserDataManager(
|
public UserDataManager(
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
IUserManager userManager,
|
IDbContextFactory<JellyfinDbContext> repository)
|
||||||
IUserDataRepository repository)
|
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_userManager = userManager;
|
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
|
_cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -59,15 +57,30 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
var keys = item.GetUserDataKeys();
|
var keys = item.GetUserDataKeys();
|
||||||
|
|
||||||
var userId = user.InternalId;
|
using var dbContext = _repository.CreateDbContext();
|
||||||
|
using var transaction = dbContext.Database.BeginTransaction();
|
||||||
|
|
||||||
foreach (var key in keys)
|
foreach (var key in keys)
|
||||||
{
|
{
|
||||||
_repository.SaveUserData(userId, key, userData, cancellationToken);
|
userData.Key = key;
|
||||||
|
var userDataEntry = Map(userData, user.Id, item.Id);
|
||||||
|
if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && f.CustomDataKey == userDataEntry.CustomDataKey))
|
||||||
|
{
|
||||||
|
dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dbContext.UserData.Add(userDataEntry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbContext.SaveChanges();
|
||||||
|
transaction.Commit();
|
||||||
|
|
||||||
|
var userId = user.InternalId;
|
||||||
var cacheKey = GetCacheKey(userId, item.Id);
|
var cacheKey = GetCacheKey(userId, item.Id);
|
||||||
_userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
|
_cache.AddOrUpdate(cacheKey, userData);
|
||||||
|
item.UserData = dbContext.UserData.Where(e => e.ItemId == item.Id).AsNoTracking().ToArray(); // rehydrate the cached userdata
|
||||||
|
|
||||||
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
|
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
|
||||||
{
|
{
|
||||||
@@ -84,10 +97,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(user);
|
ArgumentNullException.ThrowIfNull(user);
|
||||||
ArgumentNullException.ThrowIfNull(item);
|
ArgumentNullException.ThrowIfNull(item);
|
||||||
ArgumentNullException.ThrowIfNull(reason);
|
|
||||||
ArgumentNullException.ThrowIfNull(userDataDto);
|
ArgumentNullException.ThrowIfNull(userDataDto);
|
||||||
|
|
||||||
var userData = GetUserData(user, item);
|
var userData = GetUserData(user, item) ?? throw new InvalidOperationException("UserData should not be null.");
|
||||||
|
|
||||||
if (userDataDto.PlaybackPositionTicks.HasValue)
|
if (userDataDto.PlaybackPositionTicks.HasValue)
|
||||||
{
|
{
|
||||||
@@ -127,33 +139,91 @@ namespace Emby.Server.Implementations.Library
|
|||||||
SaveUserData(user, item, userData, reason, CancellationToken.None);
|
SaveUserData(user, item, userData, reason, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserItemData GetUserData(User user, Guid itemId, List<string> keys)
|
private UserData Map(UserItemData dto, Guid userId, Guid itemId)
|
||||||
{
|
{
|
||||||
var userId = user.InternalId;
|
return new UserData()
|
||||||
|
{
|
||||||
var cacheKey = GetCacheKey(userId, itemId);
|
ItemId = itemId,
|
||||||
|
CustomDataKey = dto.Key,
|
||||||
return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys));
|
Item = null,
|
||||||
|
User = null,
|
||||||
|
AudioStreamIndex = dto.AudioStreamIndex,
|
||||||
|
IsFavorite = dto.IsFavorite,
|
||||||
|
LastPlayedDate = dto.LastPlayedDate,
|
||||||
|
Likes = dto.Likes,
|
||||||
|
PlaybackPositionTicks = dto.PlaybackPositionTicks,
|
||||||
|
PlayCount = dto.PlayCount,
|
||||||
|
Played = dto.Played,
|
||||||
|
Rating = dto.Rating,
|
||||||
|
UserId = userId,
|
||||||
|
SubtitleStreamIndex = dto.SubtitleStreamIndex,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
|
private static UserItemData Map(UserData dto)
|
||||||
{
|
{
|
||||||
var userData = _repository.GetUserData(internalUserId, keys);
|
return new UserItemData()
|
||||||
|
|
||||||
if (userData is not null)
|
|
||||||
{
|
{
|
||||||
return userData;
|
Key = dto.CustomDataKey!,
|
||||||
|
AudioStreamIndex = dto.AudioStreamIndex,
|
||||||
|
IsFavorite = dto.IsFavorite,
|
||||||
|
LastPlayedDate = dto.LastPlayedDate,
|
||||||
|
Likes = dto.Likes,
|
||||||
|
PlaybackPositionTicks = dto.PlaybackPositionTicks,
|
||||||
|
PlayCount = dto.PlayCount,
|
||||||
|
Played = dto.Played,
|
||||||
|
Rating = dto.Rating,
|
||||||
|
SubtitleStreamIndex = dto.SubtitleStreamIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
|
||||||
|
{
|
||||||
|
var cacheKey = GetCacheKey(user.InternalId, itemId);
|
||||||
|
|
||||||
|
if (_cache.TryGet(cacheKey, out var data))
|
||||||
|
{
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys.Count > 0)
|
data = GetUserDataInternal(user.Id, itemId, keys);
|
||||||
|
|
||||||
|
if (data is null)
|
||||||
{
|
{
|
||||||
return new UserItemData
|
return new UserItemData()
|
||||||
{
|
{
|
||||||
Key = keys[0]
|
Key = keys[0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnreachableException();
|
return _cache.GetOrAdd(cacheKey, _ => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
|
||||||
|
{
|
||||||
|
if (keys.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var context = _repository.CreateDbContext();
|
||||||
|
var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray();
|
||||||
|
|
||||||
|
if (userData.Length > 0)
|
||||||
|
{
|
||||||
|
var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
|
||||||
|
if (directDataReference is not null)
|
||||||
|
{
|
||||||
|
return Map(directDataReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Map(userData.First());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UserItemData
|
||||||
|
{
|
||||||
|
Key = keys.Last()!
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -166,20 +236,28 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public UserItemData GetUserData(User user, BaseItem item)
|
public UserItemData? GetUserData(User user, BaseItem item)
|
||||||
{
|
{
|
||||||
return GetUserData(user, item.Id, item.GetUserDataKeys());
|
return item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData()
|
||||||
|
{
|
||||||
|
Key = item.GetUserDataKeys()[0],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public UserItemDataDto GetUserDataDto(BaseItem item, User user)
|
public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
|
||||||
=> GetUserDataDto(item, null, user, new DtoOptions());
|
=> GetUserDataDto(item, null, user, new DtoOptions());
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
|
public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
|
||||||
{
|
{
|
||||||
var userData = GetUserData(user, item);
|
var userData = GetUserData(user, item);
|
||||||
var dto = GetUserItemDataDto(userData);
|
if (userData is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dto = GetUserItemDataDto(userData, item.Id);
|
||||||
|
|
||||||
item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
|
item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
|
||||||
return dto;
|
return dto;
|
||||||
@@ -189,9 +267,10 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// Converts a UserItemData to a DTOUserItemData.
|
/// Converts a UserItemData to a DTOUserItemData.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">The data.</param>
|
/// <param name="data">The data.</param>
|
||||||
|
/// <param name="itemId">The reference key to an Item.</param>
|
||||||
/// <returns>DtoUserItemData.</returns>
|
/// <returns>DtoUserItemData.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
|
||||||
private UserItemDataDto GetUserItemDataDto(UserItemData data)
|
private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(data);
|
ArgumentNullException.ThrowIfNull(data);
|
||||||
|
|
||||||
@@ -204,6 +283,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
Rating = data.Rating,
|
Rating = data.Rating,
|
||||||
Played = data.Played,
|
Played = data.Played,
|
||||||
LastPlayedDate = data.LastPlayedDate,
|
LastPlayedDate = data.LastPlayedDate,
|
||||||
|
ItemId = itemId,
|
||||||
Key = data.Key
|
Key = data.Key
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -228,7 +308,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
// ignore progress during the beginning
|
// ignore progress during the beginning
|
||||||
positionTicks = 0;
|
positionTicks = 0;
|
||||||
}
|
}
|
||||||
else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks)
|
else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= (runtimeTicks - TimeSpan.TicksPerSecond))
|
||||||
{
|
{
|
||||||
// mark as completed close to the end
|
// mark as completed close to the end
|
||||||
positionTicks = 0;
|
positionTicks = 0;
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
@@ -308,39 +310,40 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaTypes = new List<MediaType>();
|
MediaType[] mediaTypes = [];
|
||||||
|
|
||||||
if (includeItemTypes.Length == 0)
|
if (includeItemTypes.Length == 0)
|
||||||
{
|
{
|
||||||
|
HashSet<MediaType> tmpMediaTypes = [];
|
||||||
foreach (var parent in parents.OfType<ICollectionFolder>())
|
foreach (var parent in parents.OfType<ICollectionFolder>())
|
||||||
{
|
{
|
||||||
switch (parent.CollectionType)
|
switch (parent.CollectionType)
|
||||||
{
|
{
|
||||||
case CollectionType.books:
|
case CollectionType.books:
|
||||||
mediaTypes.Add(MediaType.Book);
|
tmpMediaTypes.Add(MediaType.Book);
|
||||||
mediaTypes.Add(MediaType.Audio);
|
tmpMediaTypes.Add(MediaType.Audio);
|
||||||
break;
|
break;
|
||||||
case CollectionType.music:
|
case CollectionType.music:
|
||||||
mediaTypes.Add(MediaType.Audio);
|
tmpMediaTypes.Add(MediaType.Audio);
|
||||||
break;
|
break;
|
||||||
case CollectionType.photos:
|
case CollectionType.photos:
|
||||||
mediaTypes.Add(MediaType.Photo);
|
tmpMediaTypes.Add(MediaType.Photo);
|
||||||
mediaTypes.Add(MediaType.Video);
|
tmpMediaTypes.Add(MediaType.Video);
|
||||||
break;
|
break;
|
||||||
case CollectionType.homevideos:
|
case CollectionType.homevideos:
|
||||||
mediaTypes.Add(MediaType.Photo);
|
tmpMediaTypes.Add(MediaType.Photo);
|
||||||
mediaTypes.Add(MediaType.Video);
|
tmpMediaTypes.Add(MediaType.Video);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
mediaTypes.Add(MediaType.Video);
|
tmpMediaTypes.Add(MediaType.Video);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaTypes = mediaTypes.Distinct().ToList();
|
mediaTypes = tmpMediaTypes.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
|
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0
|
||||||
? new[]
|
? new[]
|
||||||
{
|
{
|
||||||
BaseItemKind.Person,
|
BaseItemKind.Person,
|
||||||
@@ -366,12 +369,31 @@ namespace Emby.Server.Implementations.Library
|
|||||||
Limit = limit * 5,
|
Limit = limit * 5,
|
||||||
IsPlayed = isPlayed,
|
IsPlayed = isPlayed,
|
||||||
DtoOptions = options,
|
DtoOptions = options,
|
||||||
MediaTypes = mediaTypes.ToArray()
|
MediaTypes = mediaTypes
|
||||||
};
|
};
|
||||||
|
|
||||||
if (parents.Count == 0)
|
if (request.GroupItems)
|
||||||
{
|
{
|
||||||
return _libraryManager.GetItemList(query, false);
|
var collectionType = parents
|
||||||
|
.Select(parent => parent switch
|
||||||
|
{
|
||||||
|
ICollectionFolder collectionFolder => collectionFolder.CollectionType,
|
||||||
|
UserView userView => userView.CollectionType,
|
||||||
|
_ => null
|
||||||
|
})
|
||||||
|
.FirstOrDefault(type => type is not null);
|
||||||
|
|
||||||
|
if (collectionType == CollectionType.tvshows)
|
||||||
|
{
|
||||||
|
query.Limit = limit;
|
||||||
|
return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collectionType == CollectionType.music)
|
||||||
|
{
|
||||||
|
query.Limit = limit;
|
||||||
|
return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _libraryManager.GetItemList(query, parents);
|
return _libraryManager.GetItemList(query, parents);
|
||||||
|
|||||||
@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Validators
|
namespace Emby.Server.Implementations.Library.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class ArtistsPostScanTask.
|
||||||
|
/// </summary>
|
||||||
|
public class ArtistsPostScanTask : ILibraryPostScanTask
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class ArtistsPostScanTask.
|
/// The _library manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ArtistsPostScanTask : ILibraryPostScanTask
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly ILogger<ArtistsValidator> _logger;
|
||||||
|
private readonly IItemRepository _itemRepo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="itemRepo">The item repository.</param>
|
||||||
|
public ArtistsPostScanTask(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
ILogger<ArtistsValidator> logger,
|
||||||
|
IItemRepository itemRepo)
|
||||||
{
|
{
|
||||||
/// <summary>
|
_libraryManager = libraryManager;
|
||||||
/// The _library manager.
|
_logger = logger;
|
||||||
/// </summary>
|
_itemRepo = itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
}
|
||||||
private readonly ILogger<ArtistsValidator> _logger;
|
|
||||||
private readonly IItemRepository _itemRepo;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
|
/// Runs the specified progress.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="progress">The progress.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <param name="itemRepo">The item repository.</param>
|
/// <returns>Task.</returns>
|
||||||
public ArtistsPostScanTask(
|
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
ILibraryManager libraryManager,
|
{
|
||||||
ILogger<ArtistsValidator> logger,
|
return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||||
IItemRepository itemRepo)
|
|
||||||
{
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_logger = logger;
|
|
||||||
_itemRepo = itemRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runs the specified progress.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="progress">The progress.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,102 +10,101 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Validators
|
namespace Emby.Server.Implementations.Library.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class ArtistsValidator.
|
||||||
|
/// </summary>
|
||||||
|
public class ArtistsValidator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class ArtistsValidator.
|
/// The library manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ArtistsValidator
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The logger.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ILogger<ArtistsValidator> _logger;
|
||||||
|
private readonly IItemRepository _itemRepo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="itemRepo">The item repository.</param>
|
||||||
|
public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
|
||||||
{
|
{
|
||||||
/// <summary>
|
_libraryManager = libraryManager;
|
||||||
/// The library manager.
|
_logger = logger;
|
||||||
/// </summary>
|
_itemRepo = itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The logger.
|
/// Runs the specified progress.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ILogger<ArtistsValidator> _logger;
|
/// <param name="progress">The progress.</param>
|
||||||
private readonly IItemRepository _itemRepo;
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>Task.</returns>
|
||||||
|
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var names = _itemRepo.GetAllArtistNames();
|
||||||
|
|
||||||
/// <summary>
|
var numComplete = 0;
|
||||||
/// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
|
var count = names.Count;
|
||||||
/// </summary>
|
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
foreach (var name in names)
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="itemRepo">The item repository.</param>
|
|
||||||
public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
|
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
try
|
||||||
_logger = logger;
|
|
||||||
_itemRepo = itemRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 names = _itemRepo.GetAllArtistNames();
|
|
||||||
|
|
||||||
var numComplete = 0;
|
|
||||||
var count = names.Count;
|
|
||||||
|
|
||||||
foreach (var name in names)
|
|
||||||
{
|
{
|
||||||
try
|
var item = _libraryManager.GetArtist(name);
|
||||||
{
|
|
||||||
var item = _libraryManager.GetArtist(name);
|
|
||||||
|
|
||||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Don't clutter the log
|
// Don't clutter the log
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
|
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
|
||||||
}
|
|
||||||
|
|
||||||
numComplete++;
|
|
||||||
double percent = numComplete;
|
|
||||||
percent /= count;
|
|
||||||
percent *= 100;
|
|
||||||
|
|
||||||
progress.Report(percent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
numComplete++;
|
||||||
|
double percent = numComplete;
|
||||||
|
percent /= count;
|
||||||
|
percent *= 100;
|
||||||
|
|
||||||
|
progress.Report(percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.MusicArtist],
|
||||||
|
IsDeadArtist = true,
|
||||||
|
IsLocked = false
|
||||||
|
}).Cast<MusicArtist>().ToList();
|
||||||
|
|
||||||
|
foreach (var item in deadEntities)
|
||||||
|
{
|
||||||
|
if (!item.IsAccessedByName)
|
||||||
{
|
{
|
||||||
IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
|
continue;
|
||||||
IsDeadArtist = true,
|
|
||||||
IsLocked = false
|
|
||||||
}).Cast<MusicArtist>().ToList();
|
|
||||||
|
|
||||||
foreach (var item in deadEntities)
|
|
||||||
{
|
|
||||||
if (!item.IsAccessedByName)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
|
|
||||||
|
|
||||||
_libraryManager.DeleteItem(
|
|
||||||
item,
|
|
||||||
new DeleteOptions
|
|
||||||
{
|
|
||||||
DeleteFileLocation = false
|
|
||||||
},
|
|
||||||
false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(100);
|
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
||||||
|
|
||||||
|
_libraryManager.DeleteItem(
|
||||||
|
item,
|
||||||
|
new DeleteOptions
|
||||||
|
{
|
||||||
|
DeleteFileLocation = false
|
||||||
|
},
|
||||||
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,153 +4,150 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Database.Implementations.Enums;
|
||||||
using MediaBrowser.Controller.Collections;
|
using MediaBrowser.Controller.Collections;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Movies;
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Querying;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Validators
|
namespace Emby.Server.Implementations.Library.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class CollectionPostScanTask.
|
||||||
|
/// </summary>
|
||||||
|
public class CollectionPostScanTask : ILibraryPostScanTask
|
||||||
{
|
{
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly ICollectionManager _collectionManager;
|
||||||
|
private readonly ILogger<CollectionPostScanTask> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class CollectionPostScanTask.
|
/// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CollectionPostScanTask : ILibraryPostScanTask
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
|
/// <param name="collectionManager">The collection manager.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
public CollectionPostScanTask(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
ICollectionManager collectionManager,
|
||||||
|
ILogger<CollectionPostScanTask> logger)
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly ICollectionManager _collectionManager;
|
_collectionManager = collectionManager;
|
||||||
private readonly ILogger<CollectionPostScanTask> _logger;
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
|
/// Runs the specified progress.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="progress">The progress.</param>
|
||||||
/// <param name="collectionManager">The collection manager.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <returns>Task.</returns>
|
||||||
public CollectionPostScanTask(
|
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
ILibraryManager libraryManager,
|
{
|
||||||
ICollectionManager collectionManager,
|
var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
|
||||||
ILogger<CollectionPostScanTask> logger)
|
|
||||||
|
foreach (var library in _libraryManager.RootFolder.Children)
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
|
||||||
_collectionManager = collectionManager;
|
{
|
||||||
_logger = logger;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startIndex = 0;
|
||||||
|
var pagesize = 1000;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var movies = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
MediaTypes = [MediaType.Video],
|
||||||
|
IncludeItemTypes = [BaseItemKind.Movie],
|
||||||
|
IsVirtualItem = false,
|
||||||
|
OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)],
|
||||||
|
Parent = library,
|
||||||
|
StartIndex = startIndex,
|
||||||
|
Limit = pagesize,
|
||||||
|
Recursive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var m in movies)
|
||||||
|
{
|
||||||
|
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
|
||||||
|
{
|
||||||
|
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
|
||||||
|
{
|
||||||
|
movieList.Add(movie.Id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movies.Count < pagesize)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
startIndex += pagesize;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var numComplete = 0;
|
||||||
/// Runs the specified progress.
|
var count = collectionNameMoviesMap.Count;
|
||||||
/// </summary>
|
|
||||||
/// <param name="progress">The progress.</param>
|
if (count == 0)
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
|
|
||||||
|
|
||||||
foreach (var library in _libraryManager.RootFolder.Children)
|
|
||||||
{
|
|
||||||
if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var startIndex = 0;
|
|
||||||
var pagesize = 1000;
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var movies = _libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
MediaTypes = new[] { MediaType.Video },
|
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Movie },
|
|
||||||
IsVirtualItem = false,
|
|
||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
|
||||||
Parent = library,
|
|
||||||
StartIndex = startIndex,
|
|
||||||
Limit = pagesize,
|
|
||||||
Recursive = true
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var m in movies)
|
|
||||||
{
|
|
||||||
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
|
|
||||||
{
|
|
||||||
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
|
|
||||||
{
|
|
||||||
movieList.Add(movie.Id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (movies.Count < pagesize)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
startIndex += pagesize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var numComplete = 0;
|
|
||||||
var count = collectionNameMoviesMap.Count;
|
|
||||||
|
|
||||||
if (count == 0)
|
|
||||||
{
|
|
||||||
progress.Report(100);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
IncludeItemTypes = new[] { BaseItemKind.BoxSet },
|
|
||||||
CollapseBoxSetItems = false,
|
|
||||||
Recursive = true
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
|
|
||||||
if (boxSet is null)
|
|
||||||
{
|
|
||||||
// won't automatically create collection if only one movie in it
|
|
||||||
if (movieIds.Count >= 2)
|
|
||||||
{
|
|
||||||
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
|
||||||
{
|
|
||||||
Name = collectionName,
|
|
||||||
IsLocked = true
|
|
||||||
});
|
|
||||||
|
|
||||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
numComplete++;
|
|
||||||
double percent = numComplete;
|
|
||||||
percent /= count;
|
|
||||||
percent *= 100;
|
|
||||||
|
|
||||||
progress.Report(percent);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.BoxSet],
|
||||||
|
CollapseBoxSetItems = false,
|
||||||
|
Recursive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
|
||||||
|
if (boxSet is null)
|
||||||
|
{
|
||||||
|
// won't automatically create collection if only one movie in it
|
||||||
|
if (movieIds.Count >= 2)
|
||||||
|
{
|
||||||
|
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
||||||
|
{
|
||||||
|
Name = collectionName,
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
numComplete++;
|
||||||
|
double percent = numComplete;
|
||||||
|
percent /= count;
|
||||||
|
percent *= 100;
|
||||||
|
|
||||||
|
progress.Report(percent);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Validators
|
namespace Emby.Server.Implementations.Library.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class GenresPostScanTask.
|
||||||
|
/// </summary>
|
||||||
|
public class GenresPostScanTask : ILibraryPostScanTask
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class GenresPostScanTask.
|
/// The _library manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GenresPostScanTask : ILibraryPostScanTask
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly ILogger<GenresValidator> _logger;
|
||||||
|
private readonly IItemRepository _itemRepo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="itemRepo">The item repository.</param>
|
||||||
|
public GenresPostScanTask(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
ILogger<GenresValidator> logger,
|
||||||
|
IItemRepository itemRepo)
|
||||||
{
|
{
|
||||||
/// <summary>
|
_libraryManager = libraryManager;
|
||||||
/// The _library manager.
|
_logger = logger;
|
||||||
/// </summary>
|
_itemRepo = itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
}
|
||||||
private readonly ILogger<GenresValidator> _logger;
|
|
||||||
private readonly IItemRepository _itemRepo;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
|
/// Runs the specified progress.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="progress">The progress.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <param name="itemRepo">The item repository.</param>
|
/// <returns>Task.</returns>
|
||||||
public GenresPostScanTask(
|
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
ILibraryManager libraryManager,
|
{
|
||||||
ILogger<GenresValidator> logger,
|
return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||||
IItemRepository itemRepo)
|
|
||||||
{
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_logger = logger;
|
|
||||||
_itemRepo = itemRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runs the specified progress.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="progress">The progress.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +1,103 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Validators
|
namespace Emby.Server.Implementations.Library.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class GenresValidator.
|
||||||
|
/// </summary>
|
||||||
|
public class GenresValidator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class GenresValidator.
|
/// The library manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GenresValidator
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IItemRepository _itemRepo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The logger.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ILogger<GenresValidator> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="GenresValidator"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="itemRepo">The item repository.</param>
|
||||||
|
public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
|
||||||
{
|
{
|
||||||
/// <summary>
|
_libraryManager = libraryManager;
|
||||||
/// The library manager.
|
_logger = logger;
|
||||||
/// </summary>
|
_itemRepo = itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
}
|
||||||
private readonly IItemRepository _itemRepo;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The logger.
|
/// Runs the specified progress.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ILogger<GenresValidator> _logger;
|
/// <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 names = _itemRepo.GetGenreNames();
|
||||||
|
|
||||||
/// <summary>
|
var numComplete = 0;
|
||||||
/// Initializes a new instance of the <see cref="GenresValidator"/> class.
|
var count = names.Count;
|
||||||
/// </summary>
|
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
foreach (var name in names)
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="itemRepo">The item repository.</param>
|
|
||||||
public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
|
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
try
|
||||||
_logger = logger;
|
|
||||||
_itemRepo = itemRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 names = _itemRepo.GetGenreNames();
|
|
||||||
|
|
||||||
var numComplete = 0;
|
|
||||||
var count = names.Count;
|
|
||||||
|
|
||||||
foreach (var name in names)
|
|
||||||
{
|
{
|
||||||
try
|
var item = _libraryManager.GetGenre(name);
|
||||||
{
|
|
||||||
var item = _libraryManager.GetGenre(name);
|
|
||||||
|
|
||||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Don't clutter the log
|
// Don't clutter the log
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error refreshing {GenreName}", name);
|
_logger.LogError(ex, "Error refreshing {GenreName}", name);
|
||||||
}
|
|
||||||
|
|
||||||
numComplete++;
|
|
||||||
double percent = numComplete;
|
|
||||||
percent /= count;
|
|
||||||
percent *= 100;
|
|
||||||
|
|
||||||
progress.Report(percent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(100);
|
numComplete++;
|
||||||
|
double percent = numComplete;
|
||||||
|
percent /= count;
|
||||||
|
percent *= 100;
|
||||||
|
|
||||||
|
progress.Report(percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
|
||||||
|
IsDeadGenre = true,
|
||||||
|
IsLocked = false
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var item in deadEntities)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
||||||
|
|
||||||
|
_libraryManager.DeleteItem(
|
||||||
|
item,
|
||||||
|
new DeleteOptions
|
||||||
|
{
|
||||||
|
DeleteFileLocation = false
|
||||||
|
},
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Validators
|
namespace Emby.Server.Implementations.Library.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class MusicGenresPostScanTask.
|
||||||
|
/// </summary>
|
||||||
|
public class MusicGenresPostScanTask : ILibraryPostScanTask
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class MusicGenresPostScanTask.
|
/// The library manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MusicGenresPostScanTask : ILibraryPostScanTask
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly ILogger<MusicGenresValidator> _logger;
|
||||||
|
private readonly IItemRepository _itemRepo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="itemRepo">The item repository.</param>
|
||||||
|
public MusicGenresPostScanTask(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
ILogger<MusicGenresValidator> logger,
|
||||||
|
IItemRepository itemRepo)
|
||||||
{
|
{
|
||||||
/// <summary>
|
_libraryManager = libraryManager;
|
||||||
/// The library manager.
|
_logger = logger;
|
||||||
/// </summary>
|
_itemRepo = itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
}
|
||||||
private readonly ILogger<MusicGenresValidator> _logger;
|
|
||||||
private readonly IItemRepository _itemRepo;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
|
/// Runs the specified progress.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
/// <param name="progress">The progress.</param>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <param name="itemRepo">The item repository.</param>
|
/// <returns>Task.</returns>
|
||||||
public MusicGenresPostScanTask(
|
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
ILibraryManager libraryManager,
|
{
|
||||||
ILogger<MusicGenresValidator> logger,
|
return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||||
IItemRepository itemRepo)
|
|
||||||
{
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_logger = logger;
|
|
||||||
_itemRepo = itemRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runs the specified progress.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="progress">The progress.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,77 +5,76 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Validators
|
namespace Emby.Server.Implementations.Library.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class MusicGenresValidator.
|
||||||
|
/// </summary>
|
||||||
|
public class MusicGenresValidator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class MusicGenresValidator.
|
/// The library manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MusicGenresValidator
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The logger.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ILogger<MusicGenresValidator> _logger;
|
||||||
|
private readonly IItemRepository _itemRepo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="itemRepo">The item repository.</param>
|
||||||
|
public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
|
||||||
{
|
{
|
||||||
/// <summary>
|
_libraryManager = libraryManager;
|
||||||
/// The library manager.
|
_logger = logger;
|
||||||
/// </summary>
|
_itemRepo = itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The logger.
|
/// Runs the specified progress.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ILogger<MusicGenresValidator> _logger;
|
/// <param name="progress">The progress.</param>
|
||||||
private readonly IItemRepository _itemRepo;
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>Task.</returns>
|
||||||
|
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var names = _itemRepo.GetMusicGenreNames();
|
||||||
|
|
||||||
/// <summary>
|
var numComplete = 0;
|
||||||
/// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
|
var count = names.Count;
|
||||||
/// </summary>
|
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
foreach (var name in names)
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="itemRepo">The item repository.</param>
|
|
||||||
public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
|
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
try
|
||||||
_logger = logger;
|
|
||||||
_itemRepo = itemRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 names = _itemRepo.GetMusicGenreNames();
|
|
||||||
|
|
||||||
var numComplete = 0;
|
|
||||||
var count = names.Count;
|
|
||||||
|
|
||||||
foreach (var name in names)
|
|
||||||
{
|
{
|
||||||
try
|
var item = _libraryManager.GetMusicGenre(name);
|
||||||
{
|
|
||||||
var item = _libraryManager.GetMusicGenre(name);
|
|
||||||
|
|
||||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Don't clutter the log
|
// Don't clutter the log
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error refreshing {GenreName}", name);
|
_logger.LogError(ex, "Error refreshing {GenreName}", name);
|
||||||
}
|
|
||||||
|
|
||||||
numComplete++;
|
|
||||||
double percent = numComplete;
|
|
||||||
percent /= count;
|
|
||||||
percent *= 100;
|
|
||||||
|
|
||||||
progress.Report(percent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(100);
|
numComplete++;
|
||||||
|
double percent = numComplete;
|
||||||
|
percent /= count;
|
||||||
|
percent *= 100;
|
||||||
|
|
||||||
|
progress.Report(percent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@@ -9,119 +9,112 @@ using MediaBrowser.Controller.Providers;
|
|||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Validators
|
namespace Emby.Server.Implementations.Library.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class PeopleValidator.
|
||||||
|
/// </summary>
|
||||||
|
public class PeopleValidator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class PeopleValidator.
|
/// The _library manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PeopleValidator
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The _logger.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
|
||||||
|
/// <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)
|
||||||
{
|
{
|
||||||
/// <summary>
|
_libraryManager = libraryManager;
|
||||||
/// The _library manager.
|
_logger = logger;
|
||||||
/// </summary>
|
_fileSystem = fileSystem;
|
||||||
private readonly ILibraryManager _libraryManager;
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _logger.
|
/// Validates the people.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ILogger _logger;
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <param name="progress">The progress.</param>
|
||||||
|
/// <returns>Task.</returns>
|
||||||
|
public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
|
||||||
|
{
|
||||||
|
var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
|
||||||
|
|
||||||
private readonly IFileSystem _fileSystem;
|
var numComplete = 0;
|
||||||
|
|
||||||
/// <summary>
|
var numPeople = people.Count;
|
||||||
/// Initializes a new instance of the <see cref="PeopleValidator" /> class.
|
|
||||||
/// </summary>
|
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
|
||||||
/// <param name="logger">The logger.</param>
|
_logger.LogDebug("Will refresh {Amount} people", numPeople);
|
||||||
/// <param name="fileSystem">The file system.</param>
|
|
||||||
public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
|
foreach (var person in people)
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
_logger = logger;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
try
|
||||||
/// Validates the people.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <param name="progress">The progress.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
|
|
||||||
{
|
|
||||||
var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
|
|
||||||
|
|
||||||
var numComplete = 0;
|
|
||||||
|
|
||||||
var numPeople = people.Count;
|
|
||||||
|
|
||||||
_logger.LogDebug("Will refresh {0} people", numPeople);
|
|
||||||
|
|
||||||
foreach (var person in people)
|
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
var item = _libraryManager.GetPerson(person);
|
||||||
|
if (item is null)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetPerson(person);
|
_logger.LogWarning("Failed to get person: {Name}", person);
|
||||||
if (item is null)
|
continue;
|
||||||
{
|
|
||||||
_logger.LogWarning("Failed to get person: {Name}", person);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
|
||||||
{
|
|
||||||
ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
|
|
||||||
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
|
|
||||||
};
|
|
||||||
|
|
||||||
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error validating IBN entry {Person}", person);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update progress
|
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||||
numComplete++;
|
{
|
||||||
double percent = numComplete;
|
ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
|
||||||
percent /= numPeople;
|
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
|
||||||
|
};
|
||||||
|
|
||||||
progress.Report(100 * percent);
|
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error validating IBN entry {Person}", person);
|
||||||
}
|
}
|
||||||
|
|
||||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
// Update progress
|
||||||
{
|
numComplete++;
|
||||||
IncludeItemTypes = [BaseItemKind.Person],
|
double percent = numComplete;
|
||||||
IsDeadPerson = true,
|
percent /= numPeople;
|
||||||
IsLocked = false
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var item in deadEntities)
|
subProgress.Report(100 * percent);
|
||||||
{
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Deleting dead {2} {0} {1}.",
|
|
||||||
item.Id.ToString("N", CultureInfo.InvariantCulture),
|
|
||||||
item.Name,
|
|
||||||
item.GetType().Name);
|
|
||||||
|
|
||||||
_libraryManager.DeleteItem(
|
|
||||||
item,
|
|
||||||
new DeleteOptions
|
|
||||||
{
|
|
||||||
DeleteFileLocation = false
|
|
||||||
},
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.Report(100);
|
|
||||||
|
|
||||||
_logger.LogInformation("People validation complete");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.Person],
|
||||||
|
IsDeadPerson = true,
|
||||||
|
IsLocked = false
|
||||||
|
});
|
||||||
|
|
||||||
|
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
foreach (var item in deadEntities.Chunk(500))
|
||||||
|
{
|
||||||
|
_libraryManager.DeleteItemsUnsafeFast(item);
|
||||||
|
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report(100);
|
||||||
|
|
||||||
|
_logger.LogInformation("People validation complete");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,46 +5,45 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Validators
|
namespace Emby.Server.Implementations.Library.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class MusicGenresPostScanTask.
|
||||||
|
/// </summary>
|
||||||
|
public class StudiosPostScanTask : ILibraryPostScanTask
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class MusicGenresPostScanTask.
|
/// The _library manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class StudiosPostScanTask : ILibraryPostScanTask
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
private readonly ILogger<StudiosValidator> _logger;
|
||||||
|
private readonly IItemRepository _itemRepo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="itemRepo">The item repository.</param>
|
||||||
|
public StudiosPostScanTask(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
ILogger<StudiosValidator> logger,
|
||||||
|
IItemRepository itemRepo)
|
||||||
{
|
{
|
||||||
/// <summary>
|
_libraryManager = libraryManager;
|
||||||
/// The _library manager.
|
_logger = logger;
|
||||||
/// </summary>
|
_itemRepo = itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
}
|
||||||
|
|
||||||
private readonly ILogger<StudiosValidator> _logger;
|
/// <summary>
|
||||||
private readonly IItemRepository _itemRepo;
|
/// Runs the specified progress.
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <param name="progress">The progress.</param>
|
||||||
/// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// </summary>
|
/// <returns>Task.</returns>
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
/// <param name="logger">The logger.</param>
|
{
|
||||||
/// <param name="itemRepo">The item repository.</param>
|
return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
||||||
public StudiosPostScanTask(
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
ILogger<StudiosValidator> logger,
|
|
||||||
IItemRepository itemRepo)
|
|
||||||
{
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_logger = logger;
|
|
||||||
_itemRepo = itemRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Runs the specified progress.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="progress">The progress.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,98 +8,97 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library.Validators
|
namespace Emby.Server.Implementations.Library.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class StudiosValidator.
|
||||||
|
/// </summary>
|
||||||
|
public class StudiosValidator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class StudiosValidator.
|
/// The library manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class StudiosValidator
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
private readonly IItemRepository _itemRepo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The logger.
|
||||||
|
/// </summary>
|
||||||
|
private readonly ILogger<StudiosValidator> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="StudiosValidator" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="itemRepo">The item repository.</param>
|
||||||
|
public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
|
||||||
{
|
{
|
||||||
/// <summary>
|
_libraryManager = libraryManager;
|
||||||
/// The library manager.
|
_logger = logger;
|
||||||
/// </summary>
|
_itemRepo = itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
}
|
||||||
|
|
||||||
private readonly IItemRepository _itemRepo;
|
/// <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 names = _itemRepo.GetStudioNames();
|
||||||
|
|
||||||
/// <summary>
|
var numComplete = 0;
|
||||||
/// The logger.
|
var count = names.Count;
|
||||||
/// </summary>
|
|
||||||
private readonly ILogger<StudiosValidator> _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
foreach (var name in names)
|
||||||
/// Initializes a new instance of the <see cref="StudiosValidator" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="itemRepo">The item repository.</param>
|
|
||||||
public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
|
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
try
|
||||||
_logger = logger;
|
|
||||||
_itemRepo = itemRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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 names = _itemRepo.GetStudioNames();
|
|
||||||
|
|
||||||
var numComplete = 0;
|
|
||||||
var count = names.Count;
|
|
||||||
|
|
||||||
foreach (var name in names)
|
|
||||||
{
|
{
|
||||||
try
|
var item = _libraryManager.GetStudio(name);
|
||||||
{
|
|
||||||
var item = _libraryManager.GetStudio(name);
|
|
||||||
|
|
||||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Don't clutter the log
|
// Don't clutter the log
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error refreshing {StudioName}", name);
|
_logger.LogError(ex, "Error refreshing {StudioName}", name);
|
||||||
}
|
|
||||||
|
|
||||||
numComplete++;
|
|
||||||
double percent = numComplete;
|
|
||||||
percent /= count;
|
|
||||||
percent *= 100;
|
|
||||||
|
|
||||||
progress.Report(percent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
numComplete++;
|
||||||
{
|
double percent = numComplete;
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Studio },
|
percent /= count;
|
||||||
IsDeadStudio = true,
|
percent *= 100;
|
||||||
IsLocked = false
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var item in deadEntities)
|
progress.Report(percent);
|
||||||
{
|
|
||||||
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
|
||||||
|
|
||||||
_libraryManager.DeleteItem(
|
|
||||||
item,
|
|
||||||
new DeleteOptions
|
|
||||||
{
|
|
||||||
DeleteFileLocation = false
|
|
||||||
},
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.Report(100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = [BaseItemKind.Studio],
|
||||||
|
IsDeadStudio = true,
|
||||||
|
IsLocked = false
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var item in deadEntities)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
||||||
|
|
||||||
|
_libraryManager.DeleteItem(
|
||||||
|
item,
|
||||||
|
new DeleteOptions
|
||||||
|
{
|
||||||
|
DeleteFileLocation = false
|
||||||
|
},
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,5 +129,11 @@
|
|||||||
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
|
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
|
||||||
"TaskAudioNormalization": "Odio Normalisering",
|
"TaskAudioNormalization": "Odio Normalisering",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
|
"TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie."
|
"TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.",
|
||||||
|
"TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
|
||||||
|
"TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
|
||||||
|
"TaskExtractMediaSegments": "Media Segment Skandeer",
|
||||||
|
"TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
|
||||||
|
"TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
|
||||||
|
"TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"Folders": "المجلدات",
|
"Folders": "المجلدات",
|
||||||
"Genres": "التصنيفات",
|
"Genres": "التصنيفات",
|
||||||
"HeaderAlbumArtists": "فناني الألبوم",
|
"HeaderAlbumArtists": "فناني الألبوم",
|
||||||
"HeaderContinueWatching": "استئناف المشاهدة",
|
"HeaderContinueWatching": "إستئناف المشاهدة",
|
||||||
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
||||||
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
||||||
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
|
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"ItemRemovedWithName": "أُزيل {0} من المكتبة",
|
"ItemRemovedWithName": "أُزيل {0} من المكتبة",
|
||||||
"LabelIpAddressValue": "عنوان الآي بي: {0}",
|
"LabelIpAddressValue": "عنوان الآي بي: {0}",
|
||||||
"LabelRunningTimeValue": "مدة التشغيل: {0}",
|
"LabelRunningTimeValue": "مدة التشغيل: {0}",
|
||||||
"Latest": "أحدث",
|
"Latest": "الأحدث",
|
||||||
"MessageApplicationUpdated": "حُدث خادم Jellyfin",
|
"MessageApplicationUpdated": "حُدث خادم Jellyfin",
|
||||||
"MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}",
|
"MessageApplicationUpdatedTo": "حُدث خادم Jellyfin إلى {0}",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}",
|
"MessageNamedServerConfigurationUpdatedWithValue": "حُدثت إعدادات الخادم في قسم {0}",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"NotificationOptionInstallationFailed": "فشل في التثبيت",
|
"NotificationOptionInstallationFailed": "فشل في التثبيت",
|
||||||
"NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا",
|
"NotificationOptionNewLibraryContent": "أُضيف محتوى جديدا",
|
||||||
"NotificationOptionPluginError": "فشل في الملحق",
|
"NotificationOptionPluginError": "فشل في الملحق",
|
||||||
"NotificationOptionPluginInstalled": "ثُبتت المكونات الإضافية",
|
"NotificationOptionPluginInstalled": "ثُبتت الملحق",
|
||||||
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
|
"NotificationOptionPluginUninstalled": "تمت إزالة الملحق",
|
||||||
"NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",
|
"NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق",
|
||||||
"NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم",
|
"NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم",
|
||||||
@@ -90,10 +90,10 @@
|
|||||||
"UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}",
|
"UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}",
|
||||||
"UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
|
"UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}",
|
||||||
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
|
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
|
||||||
"ValueSpecialEpisodeName": "حلقه خاصه - {0}",
|
"ValueSpecialEpisodeName": "حلقة خاصه - {0}",
|
||||||
"VersionNumber": "الإصدار {0}",
|
"VersionNumber": "الإصدار {0}",
|
||||||
"TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
|
"TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
|
||||||
"TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة",
|
"TaskCleanCache": "حذف الملفات المؤقتة",
|
||||||
"TasksChannelsCategory": "قنوات الإنترنت",
|
"TasksChannelsCategory": "قنوات الإنترنت",
|
||||||
"TasksLibraryCategory": "مكتبة",
|
"TasksLibraryCategory": "مكتبة",
|
||||||
"TasksMaintenanceCategory": "صيانة",
|
"TasksMaintenanceCategory": "صيانة",
|
||||||
@@ -125,11 +125,18 @@
|
|||||||
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
|
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
|
||||||
"External": "خارجي",
|
"External": "خارجي",
|
||||||
"HearingImpaired": "ضعاف السمع",
|
"HearingImpaired": "ضعاف السمع",
|
||||||
"TaskRefreshTrickplayImages": "توليد صور Trickplay",
|
"TaskRefreshTrickplayImages": "توليد صور المعاينة السريعة",
|
||||||
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
|
"TaskRefreshTrickplayImagesDescription": "يُولّد معاينات تنقل سريع لمقاطع الفيديو ضمن المكتبات المفعّلة.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
|
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
|
||||||
"TaskAudioNormalization": "تطبيع الصوت",
|
"TaskAudioNormalization": "تسوية الصوت",
|
||||||
"TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت.",
|
"TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت.",
|
||||||
"TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة"
|
"TaskDownloadMissingLyrics": "تنزيل عبارات القصيدة",
|
||||||
|
"TaskDownloadMissingLyricsDescription": "كلمات",
|
||||||
|
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
|
||||||
|
"TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
|
||||||
|
"TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
|
||||||
|
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
|
||||||
|
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
|
||||||
|
"CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user