mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-15 23:58:57 +00:00
Compare commits
1432 Commits
v10.8.0-al
...
v10.8.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
383d514353 | ||
|
|
6fc8237242 | ||
|
|
79d7a4d4df | ||
|
|
e90031b4cc | ||
|
|
4f3d562d75 | ||
|
|
6c8b40f413 | ||
|
|
ec81dc9be2 | ||
|
|
45f3fb1cfc | ||
|
|
f83a24ec43 | ||
|
|
84c03a2d93 | ||
|
|
bc8e249080 | ||
|
|
5ea9a74289 | ||
|
|
987d31ea16 | ||
|
|
f850779781 | ||
|
|
3bdc2bff5f | ||
|
|
5c6a84549a | ||
|
|
48da35f91f | ||
|
|
39b29eb9f1 | ||
|
|
a6740bf51e | ||
|
|
c7797d3ead | ||
|
|
c86d5838be | ||
|
|
43223b9036 | ||
|
|
c71385d2db | ||
|
|
ad5becc524 | ||
|
|
7c75dcfb9c | ||
|
|
7937e31a9b | ||
|
|
1f1f26306b | ||
|
|
577399ca05 | ||
|
|
e7ea7c0383 | ||
|
|
9fe7751d05 | ||
|
|
bf129ab9b8 | ||
|
|
6d23de64c0 | ||
|
|
e4f48bb486 | ||
|
|
866b4460b1 | ||
|
|
9db0b275ff | ||
|
|
14008fd7d0 | ||
|
|
8532d88a71 | ||
|
|
737c739d33 | ||
|
|
679e83082f | ||
|
|
d8e53f35a5 | ||
|
|
7a0e7b3cf8 | ||
|
|
373c63bcc7 | ||
|
|
be5d343efb | ||
|
|
c9c91cc34e | ||
|
|
774b4a0d3f | ||
|
|
a26cded0f5 | ||
|
|
4ec82ec662 | ||
|
|
879787212e | ||
|
|
23100c9b86 | ||
|
|
e6124bc154 | ||
|
|
8753b7200f | ||
|
|
88d5230bab | ||
|
|
2920c52d61 | ||
|
|
de196a7687 | ||
|
|
ba026716c1 | ||
|
|
125ee88311 | ||
|
|
c53f6a2890 | ||
|
|
649b4c49e0 | ||
|
|
848ea703bc | ||
|
|
0adadff3e7 | ||
|
|
ffdc3a6734 | ||
|
|
a51cd4f8db | ||
|
|
af87706379 | ||
|
|
8422ab687b | ||
|
|
64753cfc7f | ||
|
|
632fb05f46 | ||
|
|
527ed0607d | ||
|
|
b59daab273 | ||
|
|
8f28d52929 | ||
|
|
749b263c48 | ||
|
|
80c68b8948 | ||
|
|
a5687793c9 | ||
|
|
b344771f8a | ||
|
|
3ff78b687d | ||
|
|
d260f30810 | ||
|
|
7ffdde9a0b | ||
|
|
e14194bfe2 | ||
|
|
3bf1a7e445 | ||
|
|
1faee43b11 | ||
|
|
31f9938e3a | ||
|
|
ae9fd4ab35 | ||
|
|
71ed7f7676 | ||
|
|
3b6e003029 | ||
|
|
9357d610b1 | ||
|
|
1d4755894e | ||
|
|
2320f06666 | ||
|
|
8296f07a39 | ||
|
|
30f6263806 | ||
|
|
a9249393e1 | ||
|
|
f49a051a5f | ||
|
|
5bcab0f0f8 | ||
|
|
c5a2ff8ac4 | ||
|
|
494ed7e4d2 | ||
|
|
dd97e6bc45 | ||
|
|
7323ccfc23 | ||
|
|
d258a87fda | ||
|
|
77a007a24d | ||
|
|
a380153f92 | ||
|
|
56c81696d3 | ||
|
|
7297431f23 | ||
|
|
f2c7bccb89 | ||
|
|
b0b4068ddf | ||
|
|
3bd2cc9860 | ||
|
|
feb035b9e0 | ||
|
|
82f362abd9 | ||
|
|
04b73cace6 | ||
|
|
3b69f38a1f | ||
|
|
126da94020 | ||
|
|
f9dffa767f | ||
|
|
444b0ea310 | ||
|
|
484427b4aa | ||
|
|
c3f0649fde | ||
|
|
e877486056 | ||
|
|
9854751137 | ||
|
|
057e8ef240 | ||
|
|
205783f46f | ||
|
|
b2fb96ffed | ||
|
|
ee22feb89a | ||
|
|
ca5979cd77 | ||
|
|
d36f49589a | ||
|
|
70f37f0527 | ||
|
|
dfe0aef530 | ||
|
|
9e31d5a73f | ||
|
|
f088ca5555 | ||
|
|
2b46917dcf | ||
|
|
7bae6eff95 | ||
|
|
d0fd23bb4b | ||
|
|
d694a6c09a | ||
|
|
58f61ed118 | ||
|
|
b9da0e7f83 | ||
|
|
7eaa0600e0 | ||
|
|
47c2c536e4 | ||
|
|
7ef9e95d75 | ||
|
|
f8ea4577ab | ||
|
|
72da42cb0a | ||
|
|
dbfa0f3027 | ||
|
|
78f437401b | ||
|
|
1db748399c | ||
|
|
a41c67d16b | ||
|
|
84a1674f39 | ||
|
|
81e535fc62 | ||
|
|
f9d26ea1bc | ||
|
|
5f3dbd8294 | ||
|
|
9cebdfdec0 | ||
|
|
891ccd7bb2 | ||
|
|
54778d875d | ||
|
|
39d185c7b1 | ||
|
|
a7d45b5d3a | ||
|
|
7efa4e38c1 | ||
|
|
506ed6940b | ||
|
|
5dbe16d3e6 | ||
|
|
4c178e9188 | ||
|
|
50bc41d84d | ||
|
|
e931f5a32b | ||
|
|
d2caed25fb | ||
|
|
3f37ef70e1 | ||
|
|
d342b79218 | ||
|
|
c35fc382d4 | ||
|
|
cb6e6879e2 | ||
|
|
a71b190142 | ||
|
|
910df89cce | ||
|
|
5f15339919 | ||
|
|
56e7b323de | ||
|
|
a3a751a4f5 | ||
|
|
c85255a615 | ||
|
|
8ea8dcf128 | ||
|
|
7884e7e829 | ||
|
|
3478554249 | ||
|
|
9898c10880 | ||
|
|
ec2ad4ec8c | ||
|
|
1ffc77b43d | ||
|
|
ae79bbc34c | ||
|
|
52704e8dd0 | ||
|
|
294ab0757e | ||
|
|
bdd52df230 | ||
|
|
73117b079c | ||
|
|
b60905f991 | ||
|
|
6d5c697183 | ||
|
|
24c56328f2 | ||
|
|
ef037ad371 | ||
|
|
a64e21f57a | ||
|
|
56e135f5e6 | ||
|
|
ae22d0b7a5 | ||
|
|
b36543275f | ||
|
|
2c0c3eb3ee | ||
|
|
f1d56aa5ce | ||
|
|
0b6fbebf72 | ||
|
|
db714f967e | ||
|
|
f020bd6f3b | ||
|
|
3275f83c3b | ||
|
|
f7813803c2 | ||
|
|
477b922e4a | ||
|
|
be72001ff9 | ||
|
|
6749313249 | ||
|
|
4ebe70cf6a | ||
|
|
c4051ac16d | ||
|
|
3491f0968b | ||
|
|
fd4ffc6ba3 | ||
|
|
5912a49d1d | ||
|
|
f336647d57 | ||
|
|
9b805c9e83 | ||
|
|
19ccf414ac | ||
|
|
0504ed9fe6 | ||
|
|
c243f588a0 | ||
|
|
46491d0813 | ||
|
|
c7c0cdad95 | ||
|
|
255f5a6707 | ||
|
|
3f497459cb | ||
|
|
5d66c84f2d | ||
|
|
052a59ac3e | ||
|
|
1b8a251991 | ||
|
|
1a787e273a | ||
|
|
16fba6035c | ||
|
|
d73e9f3af5 | ||
|
|
2888080098 | ||
|
|
c8282e8441 | ||
|
|
b295b0478c | ||
|
|
42aaea3556 | ||
|
|
07b39655eb | ||
|
|
0f75f17736 | ||
|
|
83d8dbf93e | ||
|
|
e5aa708cb9 | ||
|
|
ac760b9c86 | ||
|
|
c07c7b753c | ||
|
|
8b69b0f521 | ||
|
|
079fac4a54 | ||
|
|
21afec3225 | ||
|
|
11c5a0b182 | ||
|
|
f318417c4f | ||
|
|
874fcaba69 | ||
|
|
5204863705 | ||
|
|
93941f9728 | ||
|
|
aa0f6cb5eb | ||
|
|
69cc1e0bd8 | ||
|
|
007856e61a | ||
|
|
b4954985be | ||
|
|
6b16d90b9b | ||
|
|
0f7ba42987 | ||
|
|
2a89683e80 | ||
|
|
8595a979a8 | ||
|
|
1900096012 | ||
|
|
0e8da3e805 | ||
|
|
84c9e7a22b | ||
|
|
be28f940b7 | ||
|
|
fb95fb1a73 | ||
|
|
910995f922 | ||
|
|
4f0666ac5c | ||
|
|
4bfadbc636 | ||
|
|
2cc896251f | ||
|
|
5e343d30e1 | ||
|
|
df6c5b6d42 | ||
|
|
754bda8f73 | ||
|
|
3721b5e985 | ||
|
|
3e8fe1ce11 | ||
|
|
77c73e241f | ||
|
|
9954cbd550 | ||
|
|
d8f1a87c85 | ||
|
|
bf0a7c374c | ||
|
|
8a6b26cd42 | ||
|
|
b369194710 | ||
|
|
293bcfb342 | ||
|
|
1c5571b24e | ||
|
|
b507d1a780 | ||
|
|
d471be8d92 | ||
|
|
3c5b4b9a27 | ||
|
|
c9491cf317 | ||
|
|
c5dae18034 | ||
|
|
ff4f624850 | ||
|
|
d29a423475 | ||
|
|
4c0510ee6d | ||
|
|
492c6bbd7e | ||
|
|
84878f537c | ||
|
|
825e6460c9 | ||
|
|
760b021032 | ||
|
|
a532a866e3 | ||
|
|
71bf567045 | ||
|
|
044ff0542b | ||
|
|
abfbd04782 | ||
|
|
8d0024ec49 | ||
|
|
8f761a64f5 | ||
|
|
a64eebe79f | ||
|
|
a82e378da9 | ||
|
|
884a59da07 | ||
|
|
5a9e5e0d5d | ||
|
|
85cfea4c50 | ||
|
|
3ea67374ae | ||
|
|
5da4bcc782 | ||
|
|
b46d61dfdf | ||
|
|
8119e4a573 | ||
|
|
de3c68d474 | ||
|
|
7aa0db24d8 | ||
|
|
be832e82cc | ||
|
|
368d10d042 | ||
|
|
9523a1682b | ||
|
|
8bb4cd017c | ||
|
|
c5f09ab650 | ||
|
|
99df9c8fcd | ||
|
|
4b563f4d7e | ||
|
|
e67d8ce077 | ||
|
|
39196bb5e2 | ||
|
|
5386f06095 | ||
|
|
5a9afb0874 | ||
|
|
029be321d1 | ||
|
|
96b7c46df4 | ||
|
|
f7ef7d9eda | ||
|
|
c69e79f64d | ||
|
|
4b1256e67b | ||
|
|
8d1d973438 | ||
|
|
60affd0965 | ||
|
|
8a1eca0913 | ||
|
|
a4e4b761d5 | ||
|
|
2be9a34b26 | ||
|
|
0c8b9091a5 | ||
|
|
128d54622a | ||
|
|
21ce0e58c6 | ||
|
|
3229ba4918 | ||
|
|
105f057512 | ||
|
|
b6a2640f22 | ||
|
|
a2abae3014 | ||
|
|
7084541508 | ||
|
|
e87240b374 | ||
|
|
057d5dfc25 | ||
|
|
12f9132975 | ||
|
|
4a1aa619d2 | ||
|
|
1002d1d193 | ||
|
|
7c91543694 | ||
|
|
b04211a707 | ||
|
|
fcb65ac38d | ||
|
|
727402f7f7 | ||
|
|
2a1ca4badc | ||
|
|
7f52f77ef5 | ||
|
|
1d5961126e | ||
|
|
ec6f7bdcff | ||
|
|
65c47c79da | ||
|
|
5d9df10e27 | ||
|
|
d45d228b36 | ||
|
|
2b7d139b5b | ||
|
|
a280ff603f | ||
|
|
9beb3aff4e | ||
|
|
066bdc1e72 | ||
|
|
5833c70725 | ||
|
|
cd93f49fa8 | ||
|
|
93009682b3 | ||
|
|
56573f14b0 | ||
|
|
9001eaa67e | ||
|
|
76010e80dd | ||
|
|
5778541d2f | ||
|
|
cb0baddde3 | ||
|
|
0ff37413b0 | ||
|
|
d5434988d7 | ||
|
|
847518701d | ||
|
|
c5212a20a3 | ||
|
|
385a0b9437 | ||
|
|
884ba4f3ed | ||
|
|
cba6a4e3f3 | ||
|
|
d5f44f7a5c | ||
|
|
bf1ccf7493 | ||
|
|
d7c548f3db | ||
|
|
a7abdca47a | ||
|
|
7a8eaa8811 | ||
|
|
045dca49d0 | ||
|
|
e9ce82445e | ||
|
|
2133c6e348 | ||
|
|
5de2db9f52 | ||
|
|
620625c4c1 | ||
|
|
e0b035e34e | ||
|
|
1946414e14 | ||
|
|
132c85e554 | ||
|
|
9d1049e83f | ||
|
|
72aca15191 | ||
|
|
577325b788 | ||
|
|
fec2cf5060 | ||
|
|
53b06ce4e3 | ||
|
|
bdb85aeecf | ||
|
|
e299adc819 | ||
|
|
0674f84e9e | ||
|
|
b6086398d3 | ||
|
|
1d585146d6 | ||
|
|
aa1b1c6bbb | ||
|
|
bebe1808ce | ||
|
|
fb2e4c2e1a | ||
|
|
784ed796ce | ||
|
|
579155a571 | ||
|
|
ca16a55a47 | ||
|
|
e2ffd41141 | ||
|
|
ca67a48140 | ||
|
|
d2ce315c1d | ||
|
|
6a6874aa16 | ||
|
|
6f45848b51 | ||
|
|
23ba15ccfb | ||
|
|
5376c37d42 | ||
|
|
a0eb1fd28b | ||
|
|
130c935bc3 | ||
|
|
003c48e351 | ||
|
|
96a03776e1 | ||
|
|
ac33adf219 | ||
|
|
4fa7f49ffc | ||
|
|
0a068b924f | ||
|
|
d80c4ee381 | ||
|
|
e6014bb8d3 | ||
|
|
8056b0e961 | ||
|
|
6a7775de6e | ||
|
|
4e91c3ebdc | ||
|
|
78a8c26690 | ||
|
|
c229f3ae0a | ||
|
|
3cea6c61f3 | ||
|
|
8197e5921e | ||
|
|
5f7ffd7863 | ||
|
|
c28025ba5e | ||
|
|
af5200857e | ||
|
|
97d3212410 | ||
|
|
8780fc0ea1 | ||
|
|
570660099b | ||
|
|
945fb8b72e | ||
|
|
16a449a023 | ||
|
|
669029595b | ||
|
|
72cfa6d7c9 | ||
|
|
7fb21132dd | ||
|
|
3bf5a7b801 | ||
|
|
74bdad82c1 | ||
|
|
c59087606c | ||
|
|
d7d36a102a | ||
|
|
b4bb82b6d7 | ||
|
|
eb44f892b8 | ||
|
|
90463d5e4f | ||
|
|
6904edf68c | ||
|
|
dc8fdb154a | ||
|
|
c512e783b3 | ||
|
|
4398475aac | ||
|
|
fb8ae0e9a1 | ||
|
|
5d44e45b90 | ||
|
|
dde984bd0e | ||
|
|
ba11add83b | ||
|
|
e86e64807d | ||
|
|
320ab8db6a | ||
|
|
60e0becc54 | ||
|
|
acd4a97e65 | ||
|
|
009e3a6c72 | ||
|
|
f8b5631d62 | ||
|
|
8871e5af4b | ||
|
|
4c117b0259 | ||
|
|
e4137a6279 | ||
|
|
1fe82d0deb | ||
|
|
588c349eb4 | ||
|
|
b9c3a497d5 | ||
|
|
dd6845b8f3 | ||
|
|
464ebf93dd | ||
|
|
93f569d286 | ||
|
|
0e2b20e6d6 | ||
|
|
e1527857e9 | ||
|
|
6a567e8c76 | ||
|
|
4b5148c69e | ||
|
|
8a827ba995 | ||
|
|
bb7068cb63 | ||
|
|
46798cb918 | ||
|
|
5ece92d635 | ||
|
|
9fbd675bed | ||
|
|
5cf0c1b8f8 | ||
|
|
c8a8a85944 | ||
|
|
9f147216be | ||
|
|
db60c9da25 | ||
|
|
75475285da | ||
|
|
18c7dc210c | ||
|
|
ca517af0d9 | ||
|
|
fccf3ac3eb | ||
|
|
1d7a524d82 | ||
|
|
6a3da3a031 | ||
|
|
456d95c878 | ||
|
|
36040832cc | ||
|
|
cb91ad184e | ||
|
|
122f6acb15 | ||
|
|
b4913aad30 | ||
|
|
6fa38e300a | ||
|
|
fbbb0368f7 | ||
|
|
67a0a998cc | ||
|
|
95e97fea01 | ||
|
|
cf47ee6261 | ||
|
|
d29c7c1d9e | ||
|
|
72165ae96f | ||
|
|
34af6c48d7 | ||
|
|
e6df698df1 | ||
|
|
8ddcdc7430 | ||
|
|
d659b9b9ab | ||
|
|
6bb50d5728 | ||
|
|
3ea4174d12 | ||
|
|
5319f1571f | ||
|
|
6e80c9b25f | ||
|
|
7fdc0e3c3d | ||
|
|
28223704f3 | ||
|
|
edeb198313 | ||
|
|
f671eeb6b2 | ||
|
|
3342c29f4f | ||
|
|
b2188c8676 | ||
|
|
53209830e7 | ||
|
|
21ef6661d6 | ||
|
|
6f25291931 | ||
|
|
1a307db7eb | ||
|
|
a5ffde0e9c | ||
|
|
ac83effd44 | ||
|
|
96de01ce01 | ||
|
|
470d175f32 | ||
|
|
388e0cba9f | ||
|
|
11fc84eebe | ||
|
|
1e80ba0462 | ||
|
|
a397cec981 | ||
|
|
b2c58338f2 | ||
|
|
965bf7332f | ||
|
|
dacbfc83ff | ||
|
|
1d018c5575 | ||
|
|
16e0d46771 | ||
|
|
4f1efb3996 | ||
|
|
c5ca29d2e2 | ||
|
|
03f1eff21a | ||
|
|
e3ea899bcc | ||
|
|
88c4e38ce2 | ||
|
|
ea7e6aeae0 | ||
|
|
f48c8d4802 | ||
|
|
0ef35286c5 | ||
|
|
1ec3e899d6 | ||
|
|
023eddbef5 | ||
|
|
10d1c7b25d | ||
|
|
de766e687d | ||
|
|
bb83e9f2d9 | ||
|
|
4c4f5dfe81 | ||
|
|
da41cd365c | ||
|
|
dbea7cac67 | ||
|
|
bbd5d11d3b | ||
|
|
c331e11c24 | ||
|
|
9ebd521754 | ||
|
|
84a3db6f84 | ||
|
|
5e779f20ee | ||
|
|
d871dded9f | ||
|
|
a3057afde8 | ||
|
|
27b81a535c | ||
|
|
1dcd537ea2 | ||
|
|
0a8bec1af4 | ||
|
|
973781c482 | ||
|
|
b37052a4a6 | ||
|
|
15cae4ef00 | ||
|
|
7af1cae883 | ||
|
|
178d00b14d | ||
|
|
bc3cb04c0b | ||
|
|
2579b2db56 | ||
|
|
3dc0cfc36e | ||
|
|
4791d56f6c | ||
|
|
b705ace262 | ||
|
|
947ff9defe | ||
|
|
414004f83d | ||
|
|
e3d9c53baa | ||
|
|
a123391e7e | ||
|
|
9f027bae41 | ||
|
|
5eda32980c | ||
|
|
76019d3a34 | ||
|
|
3bf83025f2 | ||
|
|
bfc27e494b | ||
|
|
f7118bebfd | ||
|
|
fab5f37e0e | ||
|
|
de4a084b03 | ||
|
|
3f6a14e1fd | ||
|
|
dad7a6fdf6 | ||
|
|
3205e97e1e | ||
|
|
136eab9b1e | ||
|
|
46ef68d589 | ||
|
|
f712c554ab | ||
|
|
e58f7ae5da | ||
|
|
92fd6475fa | ||
|
|
8121635d8c | ||
|
|
9025cd13c5 | ||
|
|
2ce0389d59 | ||
|
|
957199dcba | ||
|
|
73805e0b4a | ||
|
|
f3de108617 | ||
|
|
94793e6c6b | ||
|
|
e09641b452 | ||
|
|
b356c2b212 | ||
|
|
2dbb32976c | ||
|
|
6e91657f01 | ||
|
|
1b3e56bae3 | ||
|
|
df70d7bdf1 | ||
|
|
d16bdfbc05 | ||
|
|
fe53243cfb | ||
|
|
6498a5baca | ||
|
|
71a9ae3150 | ||
|
|
4239f80c81 | ||
|
|
f0c28019dc | ||
|
|
ba8e478a2f | ||
|
|
0a953aca8a | ||
|
|
7d226e8eef | ||
|
|
eff3d3e67e | ||
|
|
ef99a53abb | ||
|
|
1e98f168c9 | ||
|
|
2bc8383968 | ||
|
|
055c63bdee | ||
|
|
7c9aa63382 | ||
|
|
5e1f956fe0 | ||
|
|
89c29a7ed8 | ||
|
|
fe99800bde | ||
|
|
3c0c7572ef | ||
|
|
3f441a44ba | ||
|
|
aec1ee5869 | ||
|
|
c0b02ff0e5 | ||
|
|
dc1b224667 | ||
|
|
fc5c6c0404 | ||
|
|
f32b2cb592 | ||
|
|
1d367712bb | ||
|
|
c9d5cfff1d | ||
|
|
847d705969 | ||
|
|
11bb834957 | ||
|
|
40e413d575 | ||
|
|
cd4d51a515 | ||
|
|
f1808f6ae4 | ||
|
|
9251c875b1 | ||
|
|
10282dc444 | ||
|
|
3fb3d7159c | ||
|
|
11092a72a4 | ||
|
|
ee24b85f49 | ||
|
|
acf30e00ce | ||
|
|
354f22d065 | ||
|
|
175ddff169 | ||
|
|
e26446f9c0 | ||
|
|
d7cbb25d0b | ||
|
|
536ad62ea8 | ||
|
|
ce62a4465a | ||
|
|
a9a3f6bf02 | ||
|
|
cf033b25f8 | ||
|
|
15053516f8 | ||
|
|
59040bfa7d | ||
|
|
d1f6a7f497 | ||
|
|
f50a250cd9 | ||
|
|
def8500dd0 | ||
|
|
06f259001c | ||
|
|
bbac59c6d6 | ||
|
|
a61b42f7ef | ||
|
|
554d1b2ca8 | ||
|
|
bbb3117f83 | ||
|
|
6a8a6a1325 | ||
|
|
36cdeaa53c | ||
|
|
a36e34fbd2 | ||
|
|
719b707281 | ||
|
|
f1878c43a4 | ||
|
|
ca5112f45a | ||
|
|
151ddd400d | ||
|
|
48a7d3f48f | ||
|
|
b92e1baa3c | ||
|
|
adad13b865 | ||
|
|
7ccf7e6157 | ||
|
|
9930efec22 | ||
|
|
0aaf2f470a | ||
|
|
23ea14aa27 | ||
|
|
5732e6188c | ||
|
|
5825a0572b | ||
|
|
4ef0099598 | ||
|
|
4acab00963 | ||
|
|
94b5334da5 | ||
|
|
dbf9e49258 | ||
|
|
5d28c5547e | ||
|
|
dbd7be091d | ||
|
|
8b2556adba | ||
|
|
1c14c86b20 | ||
|
|
3cb49d6df0 | ||
|
|
68e7072698 | ||
|
|
69e7deb07d | ||
|
|
8ec71c094a | ||
|
|
8a2ebae74d | ||
|
|
603b6fe173 | ||
|
|
52c61bd06f | ||
|
|
ab40554759 | ||
|
|
b7cab46b4a | ||
|
|
bb7916767c | ||
|
|
a00e6ff426 | ||
|
|
b98cc71c3b | ||
|
|
103a8e69a7 | ||
|
|
75cdfe4daf | ||
|
|
d855b6872a | ||
|
|
fdfcb45c2a | ||
|
|
7885167f54 | ||
|
|
ea7e5e639d | ||
|
|
08e90039b1 | ||
|
|
7649391b9d | ||
|
|
0c89459d5b | ||
|
|
b6489e73ab | ||
|
|
10d52d16a8 | ||
|
|
f3c3402984 | ||
|
|
2b769aae69 | ||
|
|
c16d71562e | ||
|
|
a2127a48ef | ||
|
|
509d66dcb5 | ||
|
|
6c53420fe3 | ||
|
|
6af7d5445f | ||
|
|
f863ca1f2d | ||
|
|
e5701c396a | ||
|
|
488ce51032 | ||
|
|
42724ef411 | ||
|
|
4a3f1a51d2 | ||
|
|
83605affa0 | ||
|
|
91d143d6ee | ||
|
|
29f92724ef | ||
|
|
614abddd64 | ||
|
|
fabe7e1fb9 | ||
|
|
887beee614 | ||
|
|
0c665af124 | ||
|
|
28f4d574f7 | ||
|
|
09b8cde6aa | ||
|
|
ea3d79c0eb | ||
|
|
3c746f2743 | ||
|
|
da49437549 | ||
|
|
26ba99e420 | ||
|
|
e7be01d7a5 | ||
|
|
2e4dd02f76 | ||
|
|
2afcaa6ae1 | ||
|
|
fbd243e315 | ||
|
|
4f1eed862e | ||
|
|
832da133d8 | ||
|
|
2dcb2f8a9f | ||
|
|
e86f778c05 | ||
|
|
cd675475bc | ||
|
|
34ee6d82fb | ||
|
|
a4246648f4 | ||
|
|
ee43b5117d | ||
|
|
a60cb280a3 | ||
|
|
375903b215 | ||
|
|
cd4587b43f | ||
|
|
5d9e1bfcea | ||
|
|
b15ab397c7 | ||
|
|
3aeae150f8 | ||
|
|
8b36bc0ade | ||
|
|
239b516659 | ||
|
|
5c3119cf02 | ||
|
|
1a32153a31 | ||
|
|
47269d5ec6 | ||
|
|
3b075a5802 | ||
|
|
ef0708d876 | ||
|
|
8b706cebef | ||
|
|
e762454787 | ||
|
|
90736ee346 | ||
|
|
000b7ba62b | ||
|
|
f87e780fb5 | ||
|
|
60fe77c089 | ||
|
|
7500c2b28b | ||
|
|
1d7ec1071f | ||
|
|
19b9646d72 | ||
|
|
f11fa59b15 | ||
|
|
d88e39b71d | ||
|
|
54549cd5b5 | ||
|
|
ea1f3fe6bd | ||
|
|
ee32e46dde | ||
|
|
5aa748058e | ||
|
|
112db30ff2 | ||
|
|
3b486fc0ee | ||
|
|
f28384ba30 | ||
|
|
ee46754238 | ||
|
|
5df6058a8e | ||
|
|
61d8d40a4a | ||
|
|
126274c4ea | ||
|
|
3f7bd7b63e | ||
|
|
aeed255bbf | ||
|
|
4df7590e52 | ||
|
|
70751722d2 | ||
|
|
c32db3ea26 | ||
|
|
a19b6a7f61 | ||
|
|
62dc4a79ff | ||
|
|
07e9568de8 | ||
|
|
7ba9a24736 | ||
|
|
b9d4cbf3e8 | ||
|
|
768b76b999 | ||
|
|
a0f248e200 | ||
|
|
f92806c246 | ||
|
|
9a5a079f42 | ||
|
|
6ffa9539bb | ||
|
|
307679afef | ||
|
|
8a36fe7ed5 | ||
|
|
0d335082c8 | ||
|
|
6520ad03f0 | ||
|
|
ecb73168b3 | ||
|
|
4823e68a94 | ||
|
|
88e02c5290 | ||
|
|
05836c8cd3 | ||
|
|
d24683f0ab | ||
|
|
8c3f98f41b | ||
|
|
d5e7e75421 | ||
|
|
54d24ddeaf | ||
|
|
188f9632f8 | ||
|
|
ddc2569258 | ||
|
|
b43f46d5c9 | ||
|
|
620a5d5e83 | ||
|
|
9b1965b48a | ||
|
|
3ea54a8009 | ||
|
|
bd2bec4d4a | ||
|
|
a26509a98a | ||
|
|
ce61dff4aa | ||
|
|
9574d13059 | ||
|
|
565ebbb643 | ||
|
|
4234a657c8 | ||
|
|
3ab0afdc6b | ||
|
|
98962cc21f | ||
|
|
c658a883a2 | ||
|
|
82260e22a2 | ||
|
|
6b4f5a8663 | ||
|
|
f8a6b3b27b | ||
|
|
904efeaddc | ||
|
|
b6b7a89bf2 | ||
|
|
77c615ba42 | ||
|
|
a2b51d551f | ||
|
|
e300b98573 | ||
|
|
75a46a1072 | ||
|
|
6aa37ebb8f | ||
|
|
072526d798 | ||
|
|
4da240d731 | ||
|
|
b5b4e2ea89 | ||
|
|
7d63377194 | ||
|
|
71ebe220f0 | ||
|
|
3eb4bbbb86 | ||
|
|
360fd70fc7 | ||
|
|
b17fe35e2e | ||
|
|
9e0958d822 | ||
|
|
9e23af5636 | ||
|
|
6ad020acb2 | ||
|
|
c70452b4a4 | ||
|
|
045ef4b726 | ||
|
|
68db3be0e7 | ||
|
|
e026ba84c5 | ||
|
|
3fb3ee074a | ||
|
|
0fd4ff4451 | ||
|
|
c934269a6e | ||
|
|
4ba168c8a1 | ||
|
|
dc222b75c5 | ||
|
|
c6a1dcf420 | ||
|
|
fddcaf17e1 | ||
|
|
b9cd9487bd | ||
|
|
fcb8ff7c0f | ||
|
|
e51777fe4b | ||
|
|
1dfbeae045 | ||
|
|
19164378f2 | ||
|
|
c81d2e9dec | ||
|
|
ce66df2c92 | ||
|
|
6d299eed49 | ||
|
|
8952819494 | ||
|
|
f802763e8f | ||
|
|
37061e5b6b | ||
|
|
1c5c0ebb0f | ||
|
|
0c4b5a5ce8 | ||
|
|
65bf393efb | ||
|
|
38d4300309 | ||
|
|
29755c9384 | ||
|
|
608a91162a | ||
|
|
853ef727da | ||
|
|
11d0c6827f | ||
|
|
0765fd568f | ||
|
|
28c2ac9cc0 | ||
|
|
58b9e5af79 | ||
|
|
1725b2ee69 | ||
|
|
25abe479eb | ||
|
|
1b9cb8cca6 | ||
|
|
c3071f8996 | ||
|
|
dc9e1ece29 | ||
|
|
76e640b0b9 | ||
|
|
226a43619f | ||
|
|
53447976f0 | ||
|
|
b2a2bdb088 | ||
|
|
bb713d1931 | ||
|
|
73a5fc6e26 | ||
|
|
019b631d7b | ||
|
|
6f4a0683ed | ||
|
|
5a1b347b83 | ||
|
|
828bf1f0ac | ||
|
|
4ec7488bab | ||
|
|
7ad1527fd0 | ||
|
|
80fce87f7a | ||
|
|
e3a7c9238d | ||
|
|
2ab3bf97ef | ||
|
|
57db188c2e | ||
|
|
2749509f00 | ||
|
|
7bfc6b5679 | ||
|
|
251b9a5235 | ||
|
|
ea8f40e84a | ||
|
|
4441513ca4 | ||
|
|
ebbde383e8 | ||
|
|
78bb581f0c | ||
|
|
a615f87680 | ||
|
|
d594f81f65 | ||
|
|
6804624c15 | ||
|
|
cf29aae690 | ||
|
|
c0ab54f0bd | ||
|
|
60e75f7b70 | ||
|
|
17e6bd5f88 | ||
|
|
42c661e30f | ||
|
|
9f5873b8c4 | ||
|
|
cbfa355e31 | ||
|
|
728a5988b3 | ||
|
|
2e7d173188 | ||
|
|
ec2645c0c0 | ||
|
|
7db753d247 | ||
|
|
d0832c60d4 | ||
|
|
b2d85a02c2 | ||
|
|
4b9c84c52e | ||
|
|
daa76d3d34 | ||
|
|
599fc9e671 | ||
|
|
17f43c8e01 | ||
|
|
634ce40c2f | ||
|
|
b5459f49d3 | ||
|
|
932c2c6665 | ||
|
|
a04ab6b876 | ||
|
|
a8a8ce4e7b | ||
|
|
8c7dd0a691 | ||
|
|
55b429edb7 | ||
|
|
cecfdeeec3 | ||
|
|
076a13abeb | ||
|
|
259bc231d6 | ||
|
|
7774cb2067 | ||
|
|
00211ca056 | ||
|
|
ca4769ab68 | ||
|
|
84ac692312 | ||
|
|
a7a7173cd5 | ||
|
|
c86b064f80 | ||
|
|
05c8834a3a | ||
|
|
9158511017 | ||
|
|
afaff1310f | ||
|
|
b880dc8a4a | ||
|
|
91292b8ea5 | ||
|
|
83a94aa612 | ||
|
|
cb41dda5b3 | ||
|
|
a633a3052f | ||
|
|
e8ae501430 | ||
|
|
db46eaa744 | ||
|
|
71f6326e1a | ||
|
|
91f3ce3109 | ||
|
|
3f3295a5d6 | ||
|
|
76c2775d8c | ||
|
|
cd760943a9 | ||
|
|
ea9fc9f9cc | ||
|
|
d9bc65469f | ||
|
|
077f13ae4c | ||
|
|
a4565da4a9 | ||
|
|
c35b704a83 | ||
|
|
55146334e8 | ||
|
|
232a148d3f | ||
|
|
968c534864 | ||
|
|
f8fcbc88fc | ||
|
|
92448ffabd | ||
|
|
923720c988 | ||
|
|
16bf4a013b | ||
|
|
b3c48e8e78 | ||
|
|
42a7318fd0 | ||
|
|
8ddba55214 | ||
|
|
0dd304f86c | ||
|
|
249a1ca955 | ||
|
|
dea5a3f3bc | ||
|
|
b5d4fdb56e | ||
|
|
e34fd15196 | ||
|
|
7405450da2 | ||
|
|
9a0618552b | ||
|
|
4a58582ad5 | ||
|
|
4c9bd905c6 | ||
|
|
5e8aaa68cf | ||
|
|
543b0127b3 | ||
|
|
87439665d7 | ||
|
|
c3c4dc6839 | ||
|
|
e575d815cc | ||
|
|
ec9cff29df | ||
|
|
0f4da9f635 | ||
|
|
0edf77994a | ||
|
|
38f0e611c8 | ||
|
|
4e03801931 | ||
|
|
250332104b | ||
|
|
0872ede57b | ||
|
|
0120d80b78 | ||
|
|
9e665ff520 | ||
|
|
5c3d0b5510 | ||
|
|
3a4e7fb075 | ||
|
|
fcf5b9b46e | ||
|
|
9a2b88cb1f | ||
|
|
c5569c701c | ||
|
|
32629cd7da | ||
|
|
2c6d6dbbf8 | ||
|
|
bdf9bdb9e6 | ||
|
|
0e8c97ed60 | ||
|
|
843ba4506e | ||
|
|
3699fa43f0 | ||
|
|
296a61cbc4 | ||
|
|
a04f8f5efb | ||
|
|
58f788e8ab | ||
|
|
7ee96a59d3 | ||
|
|
510f92f4c5 | ||
|
|
a90614d194 | ||
|
|
5e8f27d473 | ||
|
|
1f02ab83cb | ||
|
|
2a82f8dc40 | ||
|
|
584cf47ef5 | ||
|
|
3223ef0e49 | ||
|
|
3797879d05 | ||
|
|
8b4a36d6f7 | ||
|
|
4d1b824651 | ||
|
|
fbc79cb4ce | ||
|
|
6d6fb1bba3 | ||
|
|
681bfbd535 | ||
|
|
220443eca1 | ||
|
|
4d446eb614 | ||
|
|
d707a201c9 | ||
|
|
7d86ef6f22 | ||
|
|
eeb8192602 | ||
|
|
de2d292197 | ||
|
|
593b2fd359 | ||
|
|
13ac3e3665 | ||
|
|
5c407dbab6 | ||
|
|
f50b2f7959 | ||
|
|
1acb9e1d45 | ||
|
|
6640f3f782 | ||
|
|
8aef2cb282 | ||
|
|
8bd331f5fa | ||
|
|
ee80602a0d | ||
|
|
03b3f08354 | ||
|
|
65833076db | ||
|
|
e18d966874 | ||
|
|
4cdb590291 | ||
|
|
d47811bdaf | ||
|
|
01a0a4a87c | ||
|
|
87a6fdf847 | ||
|
|
3513f5a84b | ||
|
|
a327b43ab7 | ||
|
|
3f69eeab27 | ||
|
|
dd8b9e9d23 | ||
|
|
6030946d78 | ||
|
|
fde84a1e00 | ||
|
|
3176a4ddd9 | ||
|
|
9cafa2cab4 | ||
|
|
c4c7d7431f | ||
|
|
f2648f3eee | ||
|
|
29095ab390 | ||
|
|
b9b3959223 | ||
|
|
9b71bf8cfe | ||
|
|
5eff80fb8c | ||
|
|
2d77c729f2 | ||
|
|
9d387c542a | ||
|
|
60f4d70cc5 | ||
|
|
bd48b74d5b | ||
|
|
838adaea48 | ||
|
|
46543ead27 | ||
|
|
707e5bab97 | ||
|
|
cf75f99f0e | ||
|
|
fa6f6515a6 | ||
|
|
0e491025ae | ||
|
|
40e05a7993 | ||
|
|
6193fdea69 | ||
|
|
ca2d94ee97 | ||
|
|
99a48554a6 | ||
|
|
120828d8d0 | ||
|
|
9cea773d29 | ||
|
|
a474e4cf3b | ||
|
|
b1025121b8 | ||
|
|
2029076ce8 | ||
|
|
5535b9c01f | ||
|
|
e446e9fde9 | ||
|
|
180e2dc329 | ||
|
|
40be86eec0 | ||
|
|
beafd6eaab | ||
|
|
f6d8c19a7a | ||
|
|
2da7777e6d | ||
|
|
a0df79d8a5 | ||
|
|
01b95cf8e6 | ||
|
|
6bbfcf1906 | ||
|
|
7b50048020 | ||
|
|
a9a53dc657 | ||
|
|
0d8170cedb | ||
|
|
1a35690834 | ||
|
|
c61b9ef05a | ||
|
|
b5b994b22f | ||
|
|
0894a6193f | ||
|
|
9d34d6339a | ||
|
|
bbf1399826 | ||
|
|
d016d483ae | ||
|
|
61b191d345 | ||
|
|
9433072f90 | ||
|
|
a3c5afa443 | ||
|
|
f1862f9b1a | ||
|
|
a68e58556c | ||
|
|
5e91f50c43 | ||
|
|
c1a8385c9c | ||
|
|
9978164438 | ||
|
|
eaa003775f | ||
|
|
e778462955 | ||
|
|
148fcb1bb8 | ||
|
|
757970bfc1 | ||
|
|
c677b4f6b7 | ||
|
|
20b0499b00 | ||
|
|
09f2456919 | ||
|
|
bcd84dedda | ||
|
|
2496c5d589 | ||
|
|
19c9319d2c | ||
|
|
6ef9ac6e53 | ||
|
|
502a5fc932 | ||
|
|
02ce95edaf | ||
|
|
5addb24e9b | ||
|
|
dc27e7618f | ||
|
|
3e1bbdd3d4 | ||
|
|
2ff786fd3d | ||
|
|
2a09d4244c | ||
|
|
beef6f0855 | ||
|
|
80c7119537 | ||
|
|
1df56335ee | ||
|
|
065d3fa837 | ||
|
|
1df5b5034b | ||
|
|
4a20ae6cb4 | ||
|
|
a87b87825d | ||
|
|
976e3160b8 | ||
|
|
4890454935 | ||
|
|
ca887518dd | ||
|
|
5b5ae1ef52 | ||
|
|
3eec137100 | ||
|
|
402a3797a4 | ||
|
|
0b871505a6 | ||
|
|
0485ff1899 | ||
|
|
69df004b9f | ||
|
|
8b9ff893b3 | ||
|
|
b3d5383073 | ||
|
|
9d5f328a09 | ||
|
|
f469ee3010 | ||
|
|
e30ac91f16 | ||
|
|
95183c365a | ||
|
|
0af5e60094 | ||
|
|
3c030740ea | ||
|
|
bb9ce98379 | ||
|
|
02fda265b8 | ||
|
|
501919df35 | ||
|
|
107909de30 | ||
|
|
052b667d6a | ||
|
|
e5ea607853 | ||
|
|
223cc8314a | ||
|
|
39e6658d01 | ||
|
|
8f051c86f7 | ||
|
|
11419cbbfd | ||
|
|
5b7e8a27fb | ||
|
|
718e4b8665 | ||
|
|
d387d4df92 | ||
|
|
1fa845de6e | ||
|
|
7126f9dffc | ||
|
|
b2b4bd82d7 | ||
|
|
03c7bcf9c6 | ||
|
|
ea355b4262 | ||
|
|
71a0abe211 | ||
|
|
d8c3b8e7f8 | ||
|
|
88baff5693 | ||
|
|
6d3b129666 | ||
|
|
8295a3be0d | ||
|
|
17a273d237 | ||
|
|
752bb88445 | ||
|
|
a263f3978b | ||
|
|
0009e5cc27 | ||
|
|
96ea865681 | ||
|
|
4a7498a0be | ||
|
|
e3f0a53f59 | ||
|
|
3734c95fd4 | ||
|
|
9ba7bf96ef | ||
|
|
bff5ff0cb8 | ||
|
|
acb86066ff | ||
|
|
263bbf897a | ||
|
|
97124f5fce | ||
|
|
6565b0cfbe | ||
|
|
b635b5a7e3 | ||
|
|
6d74c83ddb | ||
|
|
7cf5767949 | ||
|
|
fa366f0099 | ||
|
|
989013c974 | ||
|
|
c09e999916 | ||
|
|
61b75c82ce | ||
|
|
773a4ae1f3 | ||
|
|
38ac84ad71 | ||
|
|
054ca6a8fb | ||
|
|
3d9f865247 | ||
|
|
5cd6fba11a | ||
|
|
f7b18d97a7 | ||
|
|
45e6e2adee | ||
|
|
704c1539a2 | ||
|
|
00b293d2e9 | ||
|
|
52e9dc66f5 | ||
|
|
3c8f7d380f | ||
|
|
0a0ddb0eaf | ||
|
|
84b8c9c2bc | ||
|
|
257e1be95f | ||
|
|
6003289717 | ||
|
|
19d285149b | ||
|
|
33a2e1bd50 | ||
|
|
b8507371d5 | ||
|
|
bddded96ff | ||
|
|
24024706bf | ||
|
|
93fd1c7075 | ||
|
|
5b1b2621ab | ||
|
|
b50c3852ef | ||
|
|
1924d0740d | ||
|
|
74459ec403 | ||
|
|
c32a421ea7 | ||
|
|
4cfe8fe588 | ||
|
|
06c82973c6 | ||
|
|
f638cd08ea | ||
|
|
31b87130a2 | ||
|
|
ca99a27611 | ||
|
|
5eb1fde88c | ||
|
|
24679af2e8 | ||
|
|
474b035d99 | ||
|
|
4f45c52674 | ||
|
|
f059be8e4d | ||
|
|
03435641c8 | ||
|
|
58be1d7759 | ||
|
|
4e0edaf544 | ||
|
|
9c74103fbe | ||
|
|
551c6f02a2 | ||
|
|
358cf48506 | ||
|
|
331d4ad849 | ||
|
|
bd32cecf7a | ||
|
|
c84f2e48b0 | ||
|
|
370b7f8e12 | ||
|
|
4cb649853d | ||
|
|
a774d1fa10 | ||
|
|
5254e74719 | ||
|
|
14b5e85461 | ||
|
|
25f1cdbcb5 | ||
|
|
34df1a030b | ||
|
|
761a4e8415 | ||
|
|
f0028c728f | ||
|
|
fb0f3c3a76 | ||
|
|
2f92ee374a | ||
|
|
bb377b1466 | ||
|
|
4a28f46cac | ||
|
|
8868b34d78 | ||
|
|
01a1209f0e | ||
|
|
5a65bc1e69 | ||
|
|
8ae5316198 | ||
|
|
1fbe1266e2 | ||
|
|
412ae7f4d2 | ||
|
|
26001fca93 | ||
|
|
8579c8f9ce | ||
|
|
1d729b2b0f | ||
|
|
f73a7a6ed8 | ||
|
|
fcfc774da1 | ||
|
|
14c072dd32 | ||
|
|
132440c683 | ||
|
|
de9bf327c6 | ||
|
|
7b1314aff5 | ||
|
|
c5e42ddcc6 | ||
|
|
3de86ffdb4 | ||
|
|
4b2c40f717 | ||
|
|
5d19c26d59 | ||
|
|
0415d1ccef | ||
|
|
d10de5b7f9 | ||
|
|
5265b3eee7 | ||
|
|
4c88bf3fe3 | ||
|
|
57d5423d64 | ||
|
|
efa76c0b63 | ||
|
|
3f09fb8d70 | ||
|
|
1d19a5be61 | ||
|
|
a7a6a22109 | ||
|
|
97508c6f42 | ||
|
|
296ba26b19 | ||
|
|
c3523e7cf7 | ||
|
|
c50c9c3dbf | ||
|
|
a90735bc5a | ||
|
|
37a04d5dbf | ||
|
|
e1f7f1405e | ||
|
|
6985a4f255 | ||
|
|
53c16c2342 | ||
|
|
c1c77c8762 | ||
|
|
6bd108877e | ||
|
|
994101fcf4 | ||
|
|
1cfd11b98c | ||
|
|
2b02b53fc0 | ||
|
|
996500b2f8 | ||
|
|
ce1c36dbf2 | ||
|
|
04040b25e0 | ||
|
|
66912deb84 | ||
|
|
ac06022e0f | ||
|
|
c2d99dc3f0 | ||
|
|
ca6211ad61 | ||
|
|
5726535a26 | ||
|
|
6f85e30475 | ||
|
|
a236f52c31 | ||
|
|
64652b6392 | ||
|
|
958a4f509c | ||
|
|
40045d2147 | ||
|
|
65971eb27e | ||
|
|
15dd23e4da | ||
|
|
1c47211a9e | ||
|
|
82e6a21f3b | ||
|
|
f03e77a4d5 | ||
|
|
5c69d110cc | ||
|
|
3906343c91 | ||
|
|
0ee43f897b | ||
|
|
4dfb7b18ae | ||
|
|
666e95e27f | ||
|
|
195831ad4a | ||
|
|
892b05c5e6 | ||
|
|
0f52896691 | ||
|
|
83859a1e6d | ||
|
|
8d8e113771 | ||
|
|
d95c281142 | ||
|
|
b217f84d50 | ||
|
|
b4bf5af7c8 | ||
|
|
c8bd676b14 | ||
|
|
3c69283e2c | ||
|
|
07b9ba2bb4 | ||
|
|
2491dd513c | ||
|
|
d3d9311f48 | ||
|
|
17264a6020 | ||
|
|
b64d9bcd40 | ||
|
|
44dc647adb | ||
|
|
0cd817bba3 | ||
|
|
1e93c6ae30 | ||
|
|
13668a6ecb | ||
|
|
c8eba90c17 | ||
|
|
6c76d30538 | ||
|
|
a870f4ce70 | ||
|
|
e3e6953c6d | ||
|
|
564990964d | ||
|
|
f91839dd8c | ||
|
|
c0bab5c173 | ||
|
|
5aadf8c291 | ||
|
|
654bd6fff1 | ||
|
|
70b9f9bf56 | ||
|
|
ad7ed95b98 | ||
|
|
924c6682b9 | ||
|
|
5c5d49ee60 | ||
|
|
4279f13c67 | ||
|
|
4fc0521d69 | ||
|
|
3398f7f953 | ||
|
|
416894008e | ||
|
|
45ceba7ad1 | ||
|
|
6822693bd6 | ||
|
|
0187500373 | ||
|
|
869d537aaa | ||
|
|
f444e93a56 | ||
|
|
149c77d9b1 | ||
|
|
2b283d249f | ||
|
|
2c42d75288 | ||
|
|
a9c38870f9 | ||
|
|
77634d3b52 | ||
|
|
eec4293e6f | ||
|
|
63eeb73608 | ||
|
|
7fcf01235c | ||
|
|
104e36f2f9 | ||
|
|
3d858955b6 | ||
|
|
bbf40d6be2 | ||
|
|
7da6bd905a | ||
|
|
838225e962 | ||
|
|
4a5e8b99a0 | ||
|
|
5529625025 | ||
|
|
967fd66ca9 | ||
|
|
a229526454 | ||
|
|
0a14279e2a | ||
|
|
76eeb8f655 | ||
|
|
6881cf6874 | ||
|
|
e8efc433fa | ||
|
|
98587da53d | ||
|
|
09909d4df2 | ||
|
|
2372931b13 | ||
|
|
8e046ce22b | ||
|
|
baafa10e87 | ||
|
|
b478b115e3 | ||
|
|
022e2d9f97 | ||
|
|
0fbd8d85c8 | ||
|
|
080b02cc4c | ||
|
|
7b89e0e3a5 | ||
|
|
9abe9e7e54 | ||
|
|
10a173c011 | ||
|
|
a22c57ff33 | ||
|
|
2f6437a987 | ||
|
|
3a89e88033 | ||
|
|
f4844c08a5 | ||
|
|
73201ed498 | ||
|
|
bcb1c9b652 | ||
|
|
0e584f6840 | ||
|
|
91204fc9f0 | ||
|
|
c534c45033 | ||
|
|
71ed47a5d3 | ||
|
|
4885f5e6c9 | ||
|
|
a6357f89ab | ||
|
|
f78f1e834c | ||
|
|
2899b77cd5 | ||
|
|
41383e6fe4 | ||
|
|
64a13a5d42 | ||
|
|
43ea4af30f | ||
|
|
30f3be1da0 | ||
|
|
3e5cb8e04e | ||
|
|
c7b25a9fe4 | ||
|
|
fa38b741ce | ||
|
|
be233b49b6 | ||
|
|
35c0801d6c | ||
|
|
ee8bd9b3b5 | ||
|
|
30230aff73 | ||
|
|
d995f0e092 | ||
|
|
6e77d50563 | ||
|
|
3531dc85ae | ||
|
|
9c15f96e12 | ||
|
|
e6c9add45a | ||
|
|
ea439c5ccf | ||
|
|
86a5e72a65 | ||
|
|
4f51a22081 | ||
|
|
153e920239 | ||
|
|
be9663ae89 | ||
|
|
d7b2fa62a3 | ||
|
|
ffe5ae8056 | ||
|
|
711db363aa | ||
|
|
957c5ee061 | ||
|
|
37e388dec1 | ||
|
|
0c9b64de4b | ||
|
|
2cd1c70e55 | ||
|
|
d5b63092ed | ||
|
|
6648b7d7da | ||
|
|
2888567ea5 | ||
|
|
97c2c523a8 | ||
|
|
e682c230bd | ||
|
|
417a7011c7 | ||
|
|
3c8abeda7d | ||
|
|
7ff52bf755 | ||
|
|
7936ea59eb | ||
|
|
2e01fb3cda | ||
|
|
2fbc1190bc | ||
|
|
a5aabbb885 | ||
|
|
975d1537d4 | ||
|
|
b4ab75cd69 | ||
|
|
a49ded84c6 | ||
|
|
2182640519 | ||
|
|
f8cfc55a36 | ||
|
|
851f610e11 | ||
|
|
78e97dbaa9 | ||
|
|
f30bbef781 | ||
|
|
2b85f43cab | ||
|
|
5741fa7dfa | ||
|
|
1d6224c9c6 | ||
|
|
a3a4689af2 | ||
|
|
107412f2f2 | ||
|
|
6b2b484987 | ||
|
|
80877aa945 | ||
|
|
c52a2f2f7b | ||
|
|
f2817fef74 |
@@ -34,7 +34,6 @@ jobs:
|
||||
inputs:
|
||||
packageType: sdk
|
||||
version: ${{ parameters.DotNetSdkVersion }}
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Install ABI CompatibilityChecker Tool'
|
||||
|
||||
@@ -54,7 +54,6 @@ jobs:
|
||||
inputs:
|
||||
packageType: sdk
|
||||
version: ${{ parameters.DotNetSdkVersion }}
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Publish Server'
|
||||
|
||||
@@ -39,6 +39,10 @@ jobs:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
|
||||
displayName: 'Build Dockerfile'
|
||||
|
||||
@@ -80,6 +84,10 @@ jobs:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download OpenAPI Spec'
|
||||
inputs:
|
||||
@@ -181,7 +189,7 @@ jobs:
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'commands'
|
||||
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
|
||||
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) &
|
||||
|
||||
- job: PublishNuget
|
||||
displayName: 'Publish NuGet packages'
|
||||
@@ -199,7 +207,6 @@ jobs:
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '6.0.x'
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build Stable Nuget packages'
|
||||
|
||||
@@ -41,7 +41,6 @@ jobs:
|
||||
inputs:
|
||||
packageType: sdk
|
||||
version: ${{ parameters.DotNetSdkVersion }}
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: SonarCloudPrepare@1
|
||||
displayName: 'Prepare analysis on SonarCloud'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../fedora/Makefile
|
||||
51
.github/ISSUE_TEMPLATE/bug_report.md
vendored
51
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,51 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**System (please complete the following information):**
|
||||
- OS: [e.g. Debian, Windows]
|
||||
- Virtualization: [e.g. Docker, KVM, LXC]
|
||||
- Clients: [Browser, Android, Fire Stick, etc.]
|
||||
- Browser: [e.g. Firefox 91, Chrome 93, Safari 13]
|
||||
- Jellyfin Version: [e.g. 10.7.6, unstable 20191231]
|
||||
- FFmpeg Version: [e.g. 4.3.2-Jellyfin]
|
||||
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
|
||||
- Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
|
||||
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
|
||||
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
|
||||
- Base URL: [e.g. none, yes: /example]
|
||||
- Networking: [e.g. Host, Bridge/NAT]
|
||||
- Storage: [e.g. local, NFS, cloud]
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Server Logs**
|
||||
<!-- Please paste any log errors. -->
|
||||
|
||||
**FFmpeg Logs**
|
||||
<!-- Please paste any log errors. -->
|
||||
|
||||
**Browser Console Logs**
|
||||
<!-- Please paste any log errors. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
106
.github/ISSUE_TEMPLATE/issue report.yml
vendored
Normal file
106
.github/ISSUE_TEMPLATE/issue report.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
name: Issue Report
|
||||
description: File an issue report
|
||||
title: "[Issue]: "
|
||||
labels: [bug, triage]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please provide as much detail as necessary, most questions may not be applicable to you. If you need real-time help, join us on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [Discord](https://discord.gg/zHBxVSXdBV).
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Please describe your bug
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: |
|
||||
The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
|
||||
|
||||
This is my issue.
|
||||
|
||||
Steps to Reproduce
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: Jellyfin Version
|
||||
description: What version of Jellyfin are you running?
|
||||
options:
|
||||
- 10.7.7
|
||||
- 10.7.z
|
||||
- 10.6.4
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version-other
|
||||
attributes:
|
||||
label: "if other:"
|
||||
placeholder: Other
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
Examples:
|
||||
- **OS**: [e.g. Debian, Windows]
|
||||
- **Virtualization**: [e.g. Docker, KVM, LXC]
|
||||
- **Clients**: [Browser, Android, Fire Stick, etc.]
|
||||
- **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
|
||||
- **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
|
||||
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
|
||||
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
|
||||
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
|
||||
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
|
||||
- **Base URL**: [e.g. none, yes: /example]
|
||||
- **Networking**: [e.g. Host, Bridge/NAT]
|
||||
- **Storage**: [e.g. local, NFS, cloud]
|
||||
value: |
|
||||
- OS:
|
||||
- Virtualization:
|
||||
- Clients:
|
||||
- Browser:
|
||||
- FFmpeg Version:
|
||||
- Playback Method:
|
||||
- Hardware Acceleration:
|
||||
- Plugins:
|
||||
- Reverse Proxy:
|
||||
- Base URL:
|
||||
- Networking:
|
||||
- Storage:
|
||||
render: markdown
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Jellyfin logs
|
||||
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
|
||||
placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: ffmpeg-logs
|
||||
attributes:
|
||||
label: FFmpeg logs
|
||||
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
|
||||
placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: browserlogs
|
||||
attributes:
|
||||
label: Please attach any browser or client logs here
|
||||
placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Please attach any screenshots here
|
||||
placeholder: Images can be pasted directly into the textbox and will be hosted by github.
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
29
.github/stale.yml
vendored
29
.github/stale.yml
vendored
@@ -1,29 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 120
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 21
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- regression
|
||||
- security
|
||||
- dotnet-3.0-future
|
||||
- roadmap
|
||||
- future
|
||||
- feature
|
||||
- enhancement
|
||||
- confirmed
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
|
||||
# Disable automatic closing of pull requests
|
||||
pulls:
|
||||
daysUntilClose: false
|
||||
7
.github/workflows/codeql-analysis.yml
vendored
7
.github/workflows/codeql-analysis.yml
vendored
@@ -20,13 +20,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
include-prerelease: true
|
||||
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
|
||||
4
.github/workflows/commands.yml
vendored
4
.github/workflows/commands.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
reactions: eyes
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
124
.github/workflows/openapi.yml
vendored
Normal file
124
.github/workflows/openapi.yml
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
name: OpenAPI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
openapi-head:
|
||||
name: OpenAPI - HEAD
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
- 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"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
|
||||
|
||||
openapi-base:
|
||||
name: OpenAPI - BASE
|
||||
if: ${{ github.base_ref != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
- 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"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
|
||||
|
||||
openapi-diff:
|
||||
name: OpenAPI - Difference
|
||||
if: ${{ github.event_name == 'pull_request_target' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- openapi-head
|
||||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
- name: Workaround openapi-diff issue
|
||||
run: |
|
||||
sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
|
||||
sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
|
||||
- name: Calculate OpenAPI difference
|
||||
uses: docker://openapitools/openapi-diff
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
|
||||
- id: read-diff
|
||||
name: Read openapi-diff output
|
||||
run: |
|
||||
body=$(cat openapi-changes.md)
|
||||
body="${body//'%'/'%25'}"
|
||||
body="${body//$'\n'/'%0A'}"
|
||||
body="${body//$'\r'/'%0D'}"
|
||||
echo ::set-output name=body::$body
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@v1
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
direction: last
|
||||
body-includes: openapi-diff-workflow-comment
|
||||
- name: Reply or edit difference comment (changed)
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ steps.read-diff.outputs.body != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!--openapi-diff-workflow-comment-->
|
||||
<details>
|
||||
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
|
||||
|
||||
${{ steps.read-diff.outputs.body }}
|
||||
|
||||
</details>
|
||||
- name: Edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!--openapi-diff-workflow-comment-->
|
||||
|
||||
No changes to OpenAPI specification found. See history of this comment for previous changes.
|
||||
27
.github/workflows/repo-stale.yaml
vendored
Normal file
27
.github/workflows/repo-stale.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Issue Stale Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
days-before-stale: 120
|
||||
days-before-pr-stale: -1
|
||||
days-before-close: 21
|
||||
days-before-pr-close: -1
|
||||
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
|
||||
stale-issue-label: stale
|
||||
stale-issue-message: |-
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
4
BannedSymbols.txt
Normal file
4
BannedSymbols.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
P:System.Threading.Tasks.Task`1.Result
|
||||
M:System.Guid.op_Equality(System.Guid,System.Guid)
|
||||
M:System.Guid.op_Inequality(System.Guid,System.Guid)
|
||||
M:System.Guid.Equals(System.Object)
|
||||
@@ -1,5 +1,6 @@
|
||||
# Jellyfin Contributors
|
||||
|
||||
- [1337joe](https://github.com/1337joe)
|
||||
- [97carmine](https://github.com/97carmine)
|
||||
- [Abbe98](https://github.com/Abbe98)
|
||||
- [agrenott](https://github.com/agrenott)
|
||||
@@ -35,6 +36,7 @@
|
||||
- [dmitrylyzo](https://github.com/dmitrylyzo)
|
||||
- [DMouse10462](https://github.com/DMouse10462)
|
||||
- [DrPandemic](https://github.com/DrPandemic)
|
||||
- [eglia](https://github.com/eglia)
|
||||
- [EraYaN](https://github.com/EraYaN)
|
||||
- [escabe](https://github.com/escabe)
|
||||
- [excelite](https://github.com/excelite)
|
||||
@@ -45,6 +47,7 @@
|
||||
- [Froghut](https://github.com/Froghut)
|
||||
- [fruhnow](https://github.com/fruhnow)
|
||||
- [geilername](https://github.com/geilername)
|
||||
- [GermanCoding](https://github.com/GermanCoding)
|
||||
- [gnattu](https://github.com/gnattu)
|
||||
- [GodTamIt](https://github.com/GodTamIt)
|
||||
- [grafixeyehero](https://github.com/grafixeyehero)
|
||||
@@ -76,6 +79,7 @@
|
||||
- [mitchfizz05](https://github.com/mitchfizz05)
|
||||
- [MrTimscampi](https://github.com/MrTimscampi)
|
||||
- [n8225](https://github.com/n8225)
|
||||
- [Nalsai](https://github.com/Nalsai)
|
||||
- [Narfinger](https://github.com/Narfinger)
|
||||
- [NathanPickard](https://github.com/NathanPickard)
|
||||
- [neilsb](https://github.com/neilsb)
|
||||
@@ -115,6 +119,7 @@
|
||||
- [ssenart](https://github.com/ssenart)
|
||||
- [stanionascu](https://github.com/stanionascu)
|
||||
- [stevehayles](https://github.com/stevehayles)
|
||||
- [StollD](https://github.com/StollD)
|
||||
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||
- [tbraeutigam](https://github.com/tbraeutigam)
|
||||
- [teacupx](https://github.com/teacupx)
|
||||
@@ -150,6 +155,10 @@
|
||||
- [ianjazz246](https://github.com/ianjazz246)
|
||||
- [peterspenler](https://github.com/peterspenler)
|
||||
- [MBR-0001](https://github.com/MBR-0001)
|
||||
- [jonas-resch](https://github.com/jonas-resch)
|
||||
- [vgambier](https://github.com/vgambier)
|
||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
@@ -216,3 +225,5 @@
|
||||
- [olsh](https://github.com/olsh)
|
||||
- [lbenini](https://github.com/lbenini)
|
||||
- [gnuyent](https://github.com/gnuyent)
|
||||
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
||||
- [Jakob Kukla](https://github.com/jakobkukla)
|
||||
|
||||
@@ -3,12 +3,19 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="$(SolutionDir)/BannedSymbols.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
15
Dockerfile
15
Dockerfile
@@ -22,10 +22,10 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases
|
||||
ARG GMMLIB_VERSION=21.2.1
|
||||
ARG IGC_VERSION=1.0.8517
|
||||
ARG NEO_VERSION=21.35.20826
|
||||
ARG LEVEL_ZERO_VERSION=1.2.20826
|
||||
ARG GMMLIB_VERSION=22.0.2
|
||||
ARG IGC_VERSION=1.0.10395
|
||||
ARG NEO_VERSION=22.08.22549
|
||||
ARG LEVEL_ZERO_VERSION=1.3.22549
|
||||
|
||||
# Install dependencies:
|
||||
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
|
||||
@@ -48,8 +48,7 @@ RUN apt-get update \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl_${NEO_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-ocloc_${NEO_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \
|
||||
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
|
||||
&& dpkg -i *.deb \
|
||||
&& cd .. \
|
||||
@@ -73,7 +72,7 @@ COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# because of changes in docker and systemd we need to not build in parallel at the moment
|
||||
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
|
||||
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
|
||||
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
||||
@@ -83,7 +82,7 @@ COPY --from=builder /jellyfin /jellyfin
|
||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config /media
|
||||
VOLUME /cache /config
|
||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
|
||||
@@ -64,7 +64,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
||||
@@ -74,7 +74,7 @@ COPY --from=builder /jellyfin /jellyfin
|
||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config /media
|
||||
VOLUME /cache /config
|
||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
|
||||
@@ -55,7 +55,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
||||
@@ -65,7 +65,7 @@ COPY --from=builder /jellyfin /jellyfin
|
||||
COPY --from=web-builder /dist /jellyfin/jellyfin-web
|
||||
|
||||
EXPOSE 8096
|
||||
VOLUME /cache /config /media
|
||||
VOLUME /cache /config
|
||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Configuration
|
||||
public DlnaOptions()
|
||||
{
|
||||
EnablePlayTo = true;
|
||||
EnableServer = true;
|
||||
EnableServer = false;
|
||||
BlastAliveMessages = true;
|
||||
SendOnlyMatchedHost = true;
|
||||
ClientDiscoveryIntervalSeconds = 60;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
@@ -13,24 +11,29 @@ namespace Emby.Dlna.ContentDirectory
|
||||
/// Initializes a new instance of the <see cref="ServerItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="item">The <see cref="BaseItem"/>.</param>
|
||||
public ServerItem(BaseItem item)
|
||||
/// <param name="stubType">The stub type.</param>
|
||||
public ServerItem(BaseItem item, StubType? stubType)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
if (item is IItemByName && item is not Folder)
|
||||
if (stubType.HasValue)
|
||||
{
|
||||
StubType = stubType;
|
||||
}
|
||||
else if (item is IItemByName and not Folder)
|
||||
{
|
||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the underlying base item.
|
||||
/// Gets the underlying base item.
|
||||
/// </summary>
|
||||
public BaseItem Item { get; set; }
|
||||
public BaseItem Item { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DLNA item type.
|
||||
/// Gets the DLNA item type.
|
||||
/// </summary>
|
||||
public StubType? StubType { get; set; }
|
||||
public StubType? StubType { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ namespace Emby.Dlna.Didl
|
||||
else
|
||||
{
|
||||
var parent = item.DisplayParentId;
|
||||
if (!parent.Equals(Guid.Empty))
|
||||
if (!parent.Equals(default))
|
||||
{
|
||||
writer.WriteAttributeString("parentID", GetClientId(parent, null));
|
||||
}
|
||||
@@ -221,6 +221,7 @@ namespace Emby.Dlna.Didl
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
@@ -376,6 +377,7 @@ namespace Emby.Dlna.Didl
|
||||
targetHeight,
|
||||
streamInfo.TargetVideoBitDepth,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
@@ -657,7 +659,7 @@ namespace Emby.Dlna.Didl
|
||||
else
|
||||
{
|
||||
var parent = folder.DisplayParentId;
|
||||
if (parent.Equals(Guid.Empty))
|
||||
if (parent.Equals(default))
|
||||
{
|
||||
writer.WriteAttributeString("parentID", "0");
|
||||
}
|
||||
@@ -729,7 +731,7 @@ namespace Emby.Dlna.Didl
|
||||
{
|
||||
if (item.PremiereDate.HasValue)
|
||||
{
|
||||
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
|
||||
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -989,7 +991,7 @@ namespace Emby.Dlna.Didl
|
||||
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
|
||||
}
|
||||
|
||||
writer.WriteString(albumArtUrlInfo.url);
|
||||
writer.WriteString(albumArtUrlInfo.Url);
|
||||
writer.WriteFullEndElement();
|
||||
|
||||
// TODO: Remove these default values
|
||||
@@ -998,7 +1000,7 @@ namespace Emby.Dlna.Didl
|
||||
_profile.MaxIconWidth ?? 48,
|
||||
_profile.MaxIconHeight ?? 48,
|
||||
"jpg");
|
||||
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
|
||||
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.Url);
|
||||
|
||||
if (!_profile.EnableAlbumArtInDidl)
|
||||
{
|
||||
@@ -1045,8 +1047,8 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
// Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail
|
||||
// rather than using a larger one when available
|
||||
var width = albumartUrlInfo.width ?? maxWidth;
|
||||
var height = albumartUrlInfo.height ?? maxHeight;
|
||||
var width = albumartUrlInfo.Width ?? maxWidth;
|
||||
var height = albumartUrlInfo.Height ?? maxHeight;
|
||||
|
||||
var contentFeatures = ContentFeatureBuilder.BuildImageHeader(_profile, format, width, height, imageInfo.IsDirectStream, org_Pn);
|
||||
|
||||
@@ -1062,7 +1064,7 @@ namespace Emby.Dlna.Didl
|
||||
"resolution",
|
||||
string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height));
|
||||
|
||||
writer.WriteString(albumartUrlInfo.url);
|
||||
writer.WriteString(albumartUrlInfo.Url);
|
||||
|
||||
writer.WriteFullEndElement();
|
||||
}
|
||||
@@ -1200,7 +1202,7 @@ namespace Emby.Dlna.Didl
|
||||
return id;
|
||||
}
|
||||
|
||||
private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
|
||||
private (string Url, int? Width, int? Height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
|
||||
{
|
||||
var url = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
|
||||
@@ -17,8 +17,7 @@ namespace Emby.Dlna.Didl
|
||||
public Filter(string filter)
|
||||
{
|
||||
_all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
_fields = filter.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public bool Contains(string field)
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
@@ -84,8 +83,7 @@ namespace Emby.Dlna
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
var list = _profiles.Values.ToList();
|
||||
return list
|
||||
return _profiles.Values
|
||||
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Item1.Info.Name)
|
||||
.Select(i => i.Item2)
|
||||
@@ -112,7 +110,7 @@ namespace Emby.Dlna
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
LogUnmatchedProfile(deviceInfo);
|
||||
_logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -122,23 +120,6 @@ namespace Emby.Dlna
|
||||
return profile;
|
||||
}
|
||||
|
||||
private void LogUnmatchedProfile(DeviceIdentification profile)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
||||
builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
|
||||
builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
|
||||
builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
|
||||
builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
|
||||
builder.Append("ModelName: ").AppendLine(profile.ModelName);
|
||||
builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
|
||||
builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
|
||||
builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
|
||||
|
||||
_logger.LogInformation(builder.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to match a device with a profile.
|
||||
/// Rules:
|
||||
@@ -244,11 +225,8 @@ namespace Emby.Dlna
|
||||
{
|
||||
try
|
||||
{
|
||||
var xmlFies = _fileSystem.GetFilePaths(path)
|
||||
return _fileSystem.GetFilePaths(path)
|
||||
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
return xmlFies
|
||||
.Select(i => ParseProfileFile(i, type))
|
||||
.Where(i => i != null)
|
||||
.ToList()!; // We just filtered out all the nulls
|
||||
@@ -270,11 +248,8 @@ namespace Emby.Dlna
|
||||
|
||||
try
|
||||
{
|
||||
DeviceProfile profile;
|
||||
|
||||
var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
|
||||
|
||||
profile = ReserializeProfile(tempProfile);
|
||||
var profile = ReserializeProfile(tempProfile);
|
||||
|
||||
profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -313,8 +288,7 @@ namespace Emby.Dlna
|
||||
{
|
||||
lock (_profiles)
|
||||
{
|
||||
var list = _profiles.Values.ToList();
|
||||
return list
|
||||
return _profiles.Values
|
||||
.Select(i => i.Item1)
|
||||
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
|
||||
.ThenBy(i => i.Info.Name);
|
||||
@@ -367,9 +341,10 @@ namespace Emby.Dlna
|
||||
Directory.CreateDirectory(systemProfilesPath);
|
||||
|
||||
var fileOptions = AsyncFile.WriteOptions;
|
||||
fileOptions.Mode = FileMode.CreateNew;
|
||||
fileOptions.Mode = FileMode.Create;
|
||||
fileOptions.PreallocationSize = length;
|
||||
using (var fileStream = new FileStream(path, fileOptions))
|
||||
var fileStream = new FileStream(path, fileOptions);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
{
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
@@ -416,7 +391,7 @@ namespace Emby.Dlna
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateProfile(DeviceProfile profile)
|
||||
public void UpdateProfile(string profileId, DeviceProfile profile)
|
||||
{
|
||||
profile = ReserializeProfile(profile);
|
||||
|
||||
@@ -430,7 +405,7 @@ namespace Emby.Dlna
|
||||
throw new ArgumentException("Profile is missing Name");
|
||||
}
|
||||
|
||||
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
|
||||
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
|
||||
var path = Path.Combine(UserProfilesPath, newFilename);
|
||||
@@ -481,7 +456,7 @@ namespace Emby.Dlna
|
||||
/// <inheritdoc />
|
||||
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
|
||||
{
|
||||
var profile = GetDefaultProfile();
|
||||
var profile = GetProfile(headers) ?? GetDefaultProfile();
|
||||
|
||||
var serverId = _appHost.SystemId;
|
||||
|
||||
|
||||
@@ -22,10 +22,18 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -72,7 +80,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -52,7 +52,6 @@ namespace Emby.Dlna.Main
|
||||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly object _syncLock = new object();
|
||||
private readonly NetworkConfiguration _netConfig;
|
||||
private readonly bool _disabled;
|
||||
|
||||
private PlayToManager _manager;
|
||||
@@ -125,8 +124,8 @@ namespace Emby.Dlna.Main
|
||||
config);
|
||||
Current = this;
|
||||
|
||||
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
||||
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
|
||||
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
|
||||
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
|
||||
|
||||
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
|
||||
{
|
||||
@@ -219,11 +218,6 @@ namespace Emby.Dlna.Main
|
||||
}
|
||||
}
|
||||
|
||||
private void LogMessage(string msg)
|
||||
{
|
||||
_logger.LogDebug(msg);
|
||||
}
|
||||
|
||||
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
|
||||
{
|
||||
try
|
||||
@@ -268,12 +262,11 @@ namespace Emby.Dlna.Main
|
||||
{
|
||||
_publisher = new SsdpDevicePublisher(
|
||||
_communicationsServer,
|
||||
_networkManager,
|
||||
MediaBrowser.Common.System.OperatingSystem.Name,
|
||||
Environment.OSVersion.VersionString,
|
||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||
{
|
||||
LogFunction = LogMessage,
|
||||
LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
|
||||
SupportPnpRootDevice = false
|
||||
};
|
||||
|
||||
@@ -318,15 +311,9 @@ namespace Emby.Dlna.Main
|
||||
|
||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||
|
||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
|
||||
|
||||
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
|
||||
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
|
||||
{
|
||||
// DLNA will only work over http, so we must reset to http:// : {port}.
|
||||
uri.Scheme = "http";
|
||||
uri.Port = _netConfig.HttpServerPortNumber;
|
||||
}
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri);
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
{
|
||||
@@ -375,7 +362,7 @@ namespace Emby.Dlna.Main
|
||||
guid = text.GetMD5();
|
||||
}
|
||||
|
||||
return guid.ToString("N", CultureInfo.InvariantCulture);
|
||||
return guid.ToString("D", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void SetProperies(SsdpDevice device, string fullDeviceType)
|
||||
@@ -412,7 +399,6 @@ namespace Emby.Dlna.Main
|
||||
_imageProcessor,
|
||||
_deviceDiscovery,
|
||||
_httpClientFactory,
|
||||
_config,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
|
||||
@@ -69,11 +69,11 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
public TransportState TransportState { get; private set; }
|
||||
|
||||
public bool IsPlaying => TransportState == TransportState.Playing;
|
||||
public bool IsPlaying => TransportState == TransportState.PLAYING;
|
||||
|
||||
public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback;
|
||||
public bool IsPaused => TransportState == TransportState.PAUSED_PLAYBACK;
|
||||
|
||||
public bool IsStopped => TransportState == TransportState.Stopped;
|
||||
public bool IsStopped => TransportState == TransportState.STOPPED;
|
||||
|
||||
public Action OnDeviceUnavailable { get; set; }
|
||||
|
||||
@@ -494,7 +494,7 @@ namespace Emby.Dlna.PlayTo
|
||||
cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
TransportState = TransportState.Paused;
|
||||
TransportState = TransportState.PAUSED_PLAYBACK;
|
||||
|
||||
RestartTimer(true);
|
||||
}
|
||||
@@ -527,7 +527,7 @@ namespace Emby.Dlna.PlayTo
|
||||
if (transportState.HasValue)
|
||||
{
|
||||
// If we're not playing anything no need to get additional data
|
||||
if (transportState.Value == TransportState.Stopped)
|
||||
if (transportState.Value == TransportState.STOPPED)
|
||||
{
|
||||
UpdateMediaInfo(null, transportState.Value);
|
||||
}
|
||||
@@ -535,9 +535,9 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var tuple = await GetPositionInfo(avCommands, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var currentObject = tuple.Item2;
|
||||
var currentObject = tuple.Track;
|
||||
|
||||
if (tuple.Item1 && currentObject == null)
|
||||
if (tuple.Success && currentObject == null)
|
||||
{
|
||||
currentObject = await GetMediaInfo(avCommands, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -556,7 +556,7 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
|
||||
// If we're not playing anything make sure we don't get data more often than necessary to keep the Session alive
|
||||
if (transportState.Value == TransportState.Stopped)
|
||||
if (transportState.Value == TransportState.STOPPED)
|
||||
{
|
||||
RestartTimerInactive();
|
||||
}
|
||||
@@ -797,7 +797,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
|
||||
if (command == null)
|
||||
@@ -1179,6 +1179,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return new Device(deviceProperties, httpClientFactory, logger);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private static DeviceIcon CreateIcon(XElement element)
|
||||
{
|
||||
if (element == null)
|
||||
@@ -1186,69 +1187,61 @@ namespace Emby.Dlna.PlayTo
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
}
|
||||
|
||||
var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
|
||||
var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
|
||||
var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
|
||||
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
|
||||
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
|
||||
|
||||
var widthValue = int.Parse(width, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
var heightValue = int.Parse(height, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
_ = int.TryParse(width, NumberStyles.Integer, CultureInfo.InvariantCulture, out var widthValue);
|
||||
_ = int.TryParse(height, NumberStyles.Integer, CultureInfo.InvariantCulture, out var heightValue);
|
||||
|
||||
return new DeviceIcon
|
||||
{
|
||||
Depth = depth,
|
||||
Depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")) ?? string.Empty,
|
||||
Height = heightValue,
|
||||
MimeType = mimeType,
|
||||
Url = url,
|
||||
MimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype")) ?? string.Empty,
|
||||
Url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")) ?? string.Empty,
|
||||
Width = widthValue
|
||||
};
|
||||
}
|
||||
|
||||
private static DeviceService Create(XElement element)
|
||||
{
|
||||
var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
|
||||
var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
|
||||
var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
|
||||
var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
|
||||
var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
|
||||
|
||||
return new DeviceService
|
||||
=> new DeviceService()
|
||||
{
|
||||
ControlUrl = controlURL,
|
||||
EventSubUrl = eventSubURL,
|
||||
ScpdUrl = scpdUrl,
|
||||
ServiceId = id,
|
||||
ServiceType = type
|
||||
ControlUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL")) ?? string.Empty,
|
||||
EventSubUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL")) ?? string.Empty,
|
||||
ScpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL")) ?? string.Empty,
|
||||
ServiceId = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId")) ?? string.Empty,
|
||||
ServiceType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType")) ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
|
||||
private void UpdateMediaInfo(UBaseObject? mediaInfo, TransportState state)
|
||||
{
|
||||
TransportState = state;
|
||||
|
||||
var previousMediaInfo = CurrentMediaInfo;
|
||||
CurrentMediaInfo = mediaInfo;
|
||||
|
||||
if (previousMediaInfo == null && mediaInfo != null)
|
||||
if (mediaInfo == null)
|
||||
{
|
||||
if (state != TransportState.Stopped)
|
||||
if (previousMediaInfo != null)
|
||||
{
|
||||
OnPlaybackStop(previousMediaInfo);
|
||||
}
|
||||
}
|
||||
else if (previousMediaInfo == null)
|
||||
{
|
||||
if (state != TransportState.STOPPED)
|
||||
{
|
||||
OnPlaybackStart(mediaInfo);
|
||||
}
|
||||
}
|
||||
else if (mediaInfo != null && previousMediaInfo != null && !mediaInfo.Equals(previousMediaInfo))
|
||||
{
|
||||
OnMediaChanged(previousMediaInfo, mediaInfo);
|
||||
}
|
||||
else if (mediaInfo == null && previousMediaInfo != null)
|
||||
{
|
||||
OnPlaybackStop(previousMediaInfo);
|
||||
}
|
||||
else if (mediaInfo != null && mediaInfo.Equals(previousMediaInfo))
|
||||
else if (mediaInfo.Equals(previousMediaInfo))
|
||||
{
|
||||
OnPlaybackProgress(mediaInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnMediaChanged(previousMediaInfo, mediaInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlaybackStart(UBaseObject mediaInfo)
|
||||
|
||||
@@ -174,13 +174,13 @@ namespace Emby.Dlna.PlayTo
|
||||
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||
var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId);
|
||||
var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId.Equals(streamInfo.ItemId));
|
||||
if (currentItemIndex >= 0)
|
||||
{
|
||||
_currentPlaylistIndex = currentItemIndex;
|
||||
}
|
||||
|
||||
await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
|
||||
await SendNextTrackMessage(currentItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -210,9 +210,9 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var duration = mediaSource == null ?
|
||||
(_device.Duration == null ? (long?)null : _device.Duration.Value.Ticks) :
|
||||
mediaSource.RunTimeTicks;
|
||||
var duration = mediaSource == null
|
||||
? _device.Duration?.Ticks
|
||||
: mediaSource.RunTimeTicks;
|
||||
|
||||
var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
|
||||
|
||||
@@ -349,7 +349,9 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
_logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
|
||||
|
||||
var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
|
||||
var user = command.ControllingUserId.Equals(default)
|
||||
? null :
|
||||
_userManager.GetUserById(command.ControllingUserId);
|
||||
|
||||
var items = new List<BaseItem>();
|
||||
foreach (var id in command.ItemIds)
|
||||
@@ -392,7 +394,7 @@ namespace Emby.Dlna.PlayTo
|
||||
_playlist.AddRange(playlist);
|
||||
}
|
||||
|
||||
if (!command.ControllingUserId.Equals(Guid.Empty))
|
||||
if (!command.ControllingUserId.Equals(default))
|
||||
{
|
||||
_sessionManager.LogSessionActivity(
|
||||
_session.Client,
|
||||
@@ -446,14 +448,16 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
if (info.Item != null && !EnableClientSideSeek(info))
|
||||
{
|
||||
var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null;
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -557,6 +561,7 @@ namespace Emby.Dlna.PlayTo
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
@@ -569,7 +574,7 @@ namespace Emby.Dlna.PlayTo
|
||||
streamInfo.TargetVideoCodecTag,
|
||||
streamInfo.IsTargetAVC);
|
||||
|
||||
return list.Count == 0 ? null : list[0];
|
||||
return list.FirstOrDefault();
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -654,7 +659,7 @@ namespace Emby.Dlna.PlayTo
|
||||
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
await SendNextTrackMessage(index, cancellationToken);
|
||||
await SendNextTrackMessage(index, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var streamInfo = currentitem.StreamInfo;
|
||||
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
||||
@@ -764,14 +769,16 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var newPosition = GetProgressPositionTicks(info) ?? 0;
|
||||
|
||||
var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null;
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (EnableClientSideSeek(newItem.StreamInfo))
|
||||
{
|
||||
@@ -793,14 +800,16 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var newPosition = GetProgressPositionTicks(info) ?? 0;
|
||||
|
||||
var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null;
|
||||
var user = _session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(_session.UserId);
|
||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex);
|
||||
|
||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
||||
{
|
||||
@@ -816,7 +825,7 @@ namespace Emby.Dlna.PlayTo
|
||||
const int Interval = 500;
|
||||
|
||||
var currentWait = 0;
|
||||
while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
|
||||
while (_device.TransportState != TransportState.PLAYING && currentWait < MaxWait)
|
||||
{
|
||||
await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
|
||||
currentWait += Interval;
|
||||
@@ -883,7 +892,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
private class StreamParams
|
||||
{
|
||||
private MediaSourceInfo mediaSource;
|
||||
private MediaSourceInfo _mediaSource;
|
||||
private IMediaSourceManager _mediaSourceManager;
|
||||
|
||||
public Guid ItemId { get; set; }
|
||||
@@ -908,24 +917,22 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
|
||||
{
|
||||
if (mediaSource != null)
|
||||
if (_mediaSource != null)
|
||||
{
|
||||
return mediaSource;
|
||||
return _mediaSource;
|
||||
}
|
||||
|
||||
var hasMediaSources = Item as IHasMediaSources;
|
||||
|
||||
if (hasMediaSources == null)
|
||||
if (Item is not IHasMediaSources)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_mediaSourceManager != null)
|
||||
{
|
||||
mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
|
||||
_mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return mediaSource;
|
||||
return _mediaSource;
|
||||
}
|
||||
|
||||
private static Guid GetItemId(string url)
|
||||
@@ -951,7 +958,7 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
}
|
||||
|
||||
return Guid.Empty;
|
||||
return default;
|
||||
}
|
||||
|
||||
public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
|
||||
@@ -966,7 +973,7 @@ namespace Emby.Dlna.PlayTo
|
||||
ItemId = GetItemId(url)
|
||||
};
|
||||
|
||||
if (request.ItemId.Equals(Guid.Empty))
|
||||
if (request.ItemId.Equals(default))
|
||||
{
|
||||
return request;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -35,7 +34,6 @@ namespace Emby.Dlna.PlayTo
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
@@ -47,7 +45,7 @@ namespace Emby.Dlna.PlayTo
|
||||
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
@@ -58,7 +56,6 @@ namespace Emby.Dlna.PlayTo
|
||||
_imageProcessor = imageProcessor;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_config = config;
|
||||
_userDataManager = userDataManager;
|
||||
_localization = localization;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
|
||||
@@ -175,7 +175,7 @@ namespace Emby.Dlna.PlayTo
|
||||
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
|
||||
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType, sendValue);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
/// <summary>
|
||||
/// Core of the AVTransport service. It defines the conceptually top-
|
||||
/// level state of the transport, for example, whether it is playing, recording, etc.
|
||||
/// </summary>
|
||||
public enum TransportState
|
||||
{
|
||||
Stopped,
|
||||
Playing,
|
||||
Transitioning,
|
||||
PausedPlayback,
|
||||
Paused
|
||||
STOPPED,
|
||||
PLAYING,
|
||||
TRANSITIONING,
|
||||
PAUSED_PLAYBACK
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,8 +167,7 @@ namespace Emby.Dlna.Profiles
|
||||
|
||||
public void AddXmlRootAttribute(string name, string value)
|
||||
{
|
||||
var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
|
||||
var list = atts.ToList();
|
||||
var list = XmlRootAttributes.ToList();
|
||||
|
||||
list.Add(new XmlAttribute
|
||||
{
|
||||
|
||||
@@ -189,7 +189,7 @@ namespace Emby.Dlna.Server
|
||||
builder.Append("<icon>");
|
||||
|
||||
builder.Append("<mimetype>")
|
||||
.Append(SecurityElement.Escape(icon.MimeType ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(icon.MimeType))
|
||||
.Append("</mimetype>");
|
||||
builder.Append("<width>")
|
||||
.Append(SecurityElement.Escape(icon.Width.ToString(CultureInfo.InvariantCulture)))
|
||||
@@ -198,7 +198,7 @@ namespace Emby.Dlna.Server
|
||||
.Append(SecurityElement.Escape(icon.Height.ToString(CultureInfo.InvariantCulture)))
|
||||
.Append("</height>");
|
||||
builder.Append("<depth>")
|
||||
.Append(SecurityElement.Escape(icon.Depth ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(icon.Depth))
|
||||
.Append("</depth>");
|
||||
builder.Append("<url>")
|
||||
.Append(BuildUrl(icon.Url))
|
||||
@@ -219,10 +219,10 @@ namespace Emby.Dlna.Server
|
||||
builder.Append("<service>");
|
||||
|
||||
builder.Append("<serviceType>")
|
||||
.Append(SecurityElement.Escape(service.ServiceType ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(service.ServiceType))
|
||||
.Append("</serviceType>");
|
||||
builder.Append("<serviceId>")
|
||||
.Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(service.ServiceId))
|
||||
.Append("</serviceId>");
|
||||
builder.Append("<SCPDURL>")
|
||||
.Append(BuildUrl(service.ScpdUrl))
|
||||
|
||||
@@ -6,8 +6,8 @@ using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Diacritics.Extensions;
|
||||
using Emby.Dlna.Didl;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Emby.Dlna.Service
|
||||
|
||||
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
|
||||
{
|
||||
ControlRequestInfo? requestInfo = null;
|
||||
ControlRequestInfo requestInfo;
|
||||
|
||||
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
|
||||
{
|
||||
@@ -64,8 +64,13 @@ namespace Emby.Dlna.Service
|
||||
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
|
||||
Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
|
||||
|
||||
return CreateControlResponse(requestInfo);
|
||||
}
|
||||
|
||||
private ControlResponse CreateControlResponse(ControlRequestInfo requestInfo)
|
||||
{
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
Encoding = Encoding.UTF8,
|
||||
@@ -112,29 +117,19 @@ namespace Emby.Dlna.Service
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.LocalName)
|
||||
if (string.Equals(reader.LocalName, "Body", StringComparison.Ordinal))
|
||||
{
|
||||
case "Body":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using var subReader = reader.ReadSubtree();
|
||||
return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
}
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
await reader.SkipAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
using var subReader = reader.ReadSubtree();
|
||||
return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await reader.SkipAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -160,17 +155,17 @@ namespace Emby.Dlna.Service
|
||||
localName = reader.LocalName;
|
||||
namespaceURI = reader.NamespaceURI;
|
||||
|
||||
if (!reader.IsEmptyElement)
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = new ControlRequestInfo(localName, namespaceURI);
|
||||
using var subReader = reader.ReadSubtree();
|
||||
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
await reader.ReadAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Emby.Dlna.Service
|
||||
builder.Append("<action>");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(item.Name))
|
||||
.Append("</name>");
|
||||
|
||||
builder.Append("<argumentList>");
|
||||
@@ -48,13 +48,13 @@ namespace Emby.Dlna.Service
|
||||
builder.Append("<argument>");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(argument.Name ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(argument.Name))
|
||||
.Append("</name>");
|
||||
builder.Append("<direction>")
|
||||
.Append(SecurityElement.Escape(argument.Direction ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(argument.Direction))
|
||||
.Append("</direction>");
|
||||
builder.Append("<relatedStateVariable>")
|
||||
.Append(SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(argument.RelatedStateVariable))
|
||||
.Append("</relatedStateVariable>");
|
||||
|
||||
builder.Append("</argument>");
|
||||
@@ -81,10 +81,10 @@ namespace Emby.Dlna.Service
|
||||
.Append("\">");
|
||||
|
||||
builder.Append("<name>")
|
||||
.Append(SecurityElement.Escape(item.Name ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(item.Name))
|
||||
.Append("</name>");
|
||||
builder.Append("<dataType>")
|
||||
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
|
||||
.Append(SecurityElement.Escape(item.DataType))
|
||||
.Append("</dataType>");
|
||||
|
||||
if (item.AllowedValues.Count > 0)
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
@@ -23,8 +27,12 @@
|
||||
|
||||
<!-- Code analysers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
@@ -26,7 +27,7 @@ namespace Emby.Drawing
|
||||
public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||
{
|
||||
// Increment this when there's a change requiring caches to be invalidated
|
||||
private const string Version = "3";
|
||||
private const char Version = '3';
|
||||
|
||||
private static readonly HashSet<string> _transparentImageTypes
|
||||
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
|
||||
@@ -101,8 +102,7 @@ namespace Emby.Drawing
|
||||
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
|
||||
{
|
||||
var file = await ProcessImage(options).ConfigureAwait(false);
|
||||
|
||||
using (var fileStream = AsyncFile.OpenRead(file.Item1))
|
||||
using (var fileStream = AsyncFile.OpenRead(file.Path))
|
||||
{
|
||||
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ namespace Emby.Drawing
|
||||
=> _transparentImageTypes.Contains(Path.GetExtension(path));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options)
|
||||
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
|
||||
{
|
||||
ItemImageInfo originalImage = options.Image;
|
||||
BaseItem item = options.Item;
|
||||
@@ -130,20 +130,22 @@ namespace Emby.Drawing
|
||||
originalImageSize = new ImageDimensions(originalImage.Width, originalImage.Height);
|
||||
}
|
||||
|
||||
var mimeType = MimeTypes.GetMimeType(originalImagePath);
|
||||
if (!_imageEncoder.SupportsImageEncoding)
|
||||
{
|
||||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||
return (originalImagePath, mimeType, dateModified);
|
||||
}
|
||||
|
||||
var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
|
||||
originalImagePath = supportedImageInfo.path;
|
||||
originalImagePath = supportedImageInfo.Path;
|
||||
|
||||
if (!File.Exists(originalImagePath))
|
||||
// Original file doesn't exist, or original file is gif.
|
||||
if (!File.Exists(originalImagePath) || string.Equals(mimeType, MediaTypeNames.Image.Gif, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||
return (originalImagePath, mimeType, dateModified);
|
||||
}
|
||||
|
||||
dateModified = supportedImageInfo.dateModified;
|
||||
dateModified = supportedImageInfo.DateModified;
|
||||
bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath));
|
||||
|
||||
bool autoOrient = false;
|
||||
@@ -243,7 +245,7 @@ namespace Emby.Drawing
|
||||
return ImageFormat.Jpg;
|
||||
}
|
||||
|
||||
private string? GetMimeType(ImageFormat format, string path)
|
||||
private string GetMimeType(ImageFormat format, string path)
|
||||
=> format switch
|
||||
{
|
||||
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
|
||||
@@ -393,7 +395,13 @@ namespace Emby.Drawing
|
||||
public string GetImageBlurHash(string path)
|
||||
{
|
||||
var size = GetImageDimensions(path);
|
||||
if (size.Width <= 0 || size.Height <= 0)
|
||||
return GetImageBlurHash(path, size);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetImageBlurHash(string path, ImageDimensions imageDimensions)
|
||||
{
|
||||
if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
@@ -401,8 +409,8 @@ namespace Emby.Drawing
|
||||
// We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
|
||||
// One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
|
||||
// See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
|
||||
float xCompF = MathF.Sqrt(16.0f * size.Width / size.Height);
|
||||
float yCompF = xCompF * size.Height / size.Width;
|
||||
float xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height);
|
||||
float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width;
|
||||
|
||||
int xComp = Math.Min((int)xCompF + 1, 9);
|
||||
int yComp = Math.Min((int)yCompF + 1, 9);
|
||||
@@ -437,47 +445,46 @@ namespace Emby.Drawing
|
||||
.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
|
||||
private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
|
||||
{
|
||||
var inputFormat = Path.GetExtension(originalImagePath)
|
||||
.TrimStart('.')
|
||||
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
|
||||
var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString();
|
||||
|
||||
// These are just jpg files renamed as tbn
|
||||
if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (originalImagePath, dateModified);
|
||||
return Task.FromResult((originalImagePath, dateModified));
|
||||
}
|
||||
|
||||
if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
||||
{
|
||||
try
|
||||
{
|
||||
string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
// TODO _mediaEncoder.ConvertImage is not implemented
|
||||
// if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
//
|
||||
// string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
|
||||
// var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
|
||||
//
|
||||
// var file = _fileSystem.GetFileInfo(outputPath);
|
||||
// if (!file.Exists)
|
||||
// {
|
||||
// await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
|
||||
// dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// dateModified = file.LastWriteTimeUtc;
|
||||
// }
|
||||
//
|
||||
// originalImagePath = outputPath;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
|
||||
// }
|
||||
// }
|
||||
|
||||
string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
|
||||
var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
|
||||
|
||||
var file = _fileSystem.GetFileInfo(outputPath);
|
||||
if (!file.Exists)
|
||||
{
|
||||
await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
|
||||
dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
dateModified = file.LastWriteTimeUtc;
|
||||
}
|
||||
|
||||
originalImagePath = outputPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
|
||||
}
|
||||
}
|
||||
|
||||
return (originalImagePath, dateModified);
|
||||
return Task.FromResult((originalImagePath, dateModified));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -43,6 +43,12 @@ namespace Emby.Drawing
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetImageBlurHash(int xComp, int yComp, string path)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
|
||||
public class AudioBookListResolver
|
||||
{
|
||||
private readonly NamingOptions _options;
|
||||
private readonly AudioBookResolver _audioBookResolver;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
|
||||
@@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
|
||||
public AudioBookListResolver(NamingOptions options)
|
||||
{
|
||||
_options = options;
|
||||
_audioBookResolver = new AudioBookResolver(_options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -31,21 +33,18 @@ namespace Emby.Naming.AudioBook
|
||||
/// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
|
||||
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
|
||||
{
|
||||
var audioBookResolver = new AudioBookResolver(_options);
|
||||
|
||||
// File with empty fullname will be sorted out here.
|
||||
var audiobookFileInfos = files
|
||||
.Select(i => audioBookResolver.Resolve(i.FullName))
|
||||
.Select(i => _audioBookResolver.Resolve(i.FullName))
|
||||
.OfType<AudioBookFileInfo>()
|
||||
.ToList();
|
||||
|
||||
var stackResult = new StackResolver(_options)
|
||||
.ResolveAudioBooks(audiobookFileInfos);
|
||||
var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
|
||||
|
||||
foreach (var stack in stackResult)
|
||||
{
|
||||
var stackFiles = stack.Files
|
||||
.Select(i => audioBookResolver.Resolve(i))
|
||||
.Select(i => _audioBookResolver.Resolve(i))
|
||||
.OfType<AudioBookFileInfo>()
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace Emby.Naming.AudioBook
|
||||
{
|
||||
@@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook
|
||||
var extension = Path.GetExtension(path);
|
||||
|
||||
// Check supported extensions
|
||||
if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#pragma warning disable CA1819
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Video;
|
||||
@@ -20,47 +23,60 @@ namespace Emby.Naming.Common
|
||||
{
|
||||
VideoFileExtensions = new[]
|
||||
{
|
||||
".m4v",
|
||||
".001",
|
||||
".3g2",
|
||||
".3gp",
|
||||
".nsv",
|
||||
".ts",
|
||||
".ty",
|
||||
".strm",
|
||||
".rm",
|
||||
".rmvb",
|
||||
".ifo",
|
||||
".mov",
|
||||
".qt",
|
||||
".divx",
|
||||
".xvid",
|
||||
".bivx",
|
||||
".vob",
|
||||
".nrg",
|
||||
".img",
|
||||
".iso",
|
||||
".pva",
|
||||
".wmv",
|
||||
".amv",
|
||||
".asf",
|
||||
".asx",
|
||||
".ogm",
|
||||
".m2v",
|
||||
".avi",
|
||||
".bin",
|
||||
".dvr-ms",
|
||||
".mpg",
|
||||
".mpeg",
|
||||
".mp4",
|
||||
".mkv",
|
||||
".avc",
|
||||
".vp3",
|
||||
".svq3",
|
||||
".nuv",
|
||||
".viv",
|
||||
".bivx",
|
||||
".divx",
|
||||
".dv",
|
||||
".dvr-ms",
|
||||
".f4v",
|
||||
".fli",
|
||||
".flv",
|
||||
".001",
|
||||
".tp"
|
||||
".ifo",
|
||||
".img",
|
||||
".iso",
|
||||
".m2t",
|
||||
".m2ts",
|
||||
".m2v",
|
||||
".m4v",
|
||||
".mkv",
|
||||
".mk3d",
|
||||
".mov",
|
||||
".mp4",
|
||||
".mpe",
|
||||
".mpeg",
|
||||
".mpg",
|
||||
".mts",
|
||||
".mxf",
|
||||
".nrg",
|
||||
".nsv",
|
||||
".nuv",
|
||||
".ogg",
|
||||
".ogm",
|
||||
".ogv",
|
||||
".pva",
|
||||
".qt",
|
||||
".rec",
|
||||
".rm",
|
||||
".rmvb",
|
||||
".strm",
|
||||
".svq3",
|
||||
".tp",
|
||||
".ts",
|
||||
".ty",
|
||||
".viv",
|
||||
".vob",
|
||||
".vp3",
|
||||
".webm",
|
||||
".wmv",
|
||||
".wtv",
|
||||
".xvid"
|
||||
};
|
||||
|
||||
VideoFlagDelimiters = new[]
|
||||
@@ -122,11 +138,11 @@ namespace Emby.Naming.Common
|
||||
token: "DSR")
|
||||
};
|
||||
|
||||
VideoFileStackingExpressions = new[]
|
||||
VideoFileStackingRules = new[]
|
||||
{
|
||||
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
||||
"(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
|
||||
"(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
|
||||
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>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
|
||||
};
|
||||
|
||||
CleanDateTimes = new[]
|
||||
@@ -146,32 +162,20 @@ namespace Emby.Naming.Common
|
||||
|
||||
SubtitleFileExtensions = new[]
|
||||
{
|
||||
".ass",
|
||||
".mks",
|
||||
".sami",
|
||||
".smi",
|
||||
".srt",
|
||||
".ssa",
|
||||
".ass",
|
||||
".sub"
|
||||
};
|
||||
|
||||
SubtitleFlagDelimiters = new[]
|
||||
{
|
||||
'.'
|
||||
};
|
||||
|
||||
SubtitleForcedFlags = new[]
|
||||
{
|
||||
"foreign",
|
||||
"forced"
|
||||
};
|
||||
|
||||
SubtitleDefaultFlags = new[]
|
||||
{
|
||||
"default"
|
||||
".sub",
|
||||
".vtt",
|
||||
};
|
||||
|
||||
AlbumStackingPrefixes = new[]
|
||||
{
|
||||
"disc",
|
||||
"cd",
|
||||
"disc",
|
||||
"disk",
|
||||
"vol",
|
||||
"volume"
|
||||
@@ -179,68 +183,101 @@ namespace Emby.Naming.Common
|
||||
|
||||
AudioFileExtensions = new[]
|
||||
{
|
||||
".nsv",
|
||||
".m4a",
|
||||
".flac",
|
||||
".aac",
|
||||
".strm",
|
||||
".pls",
|
||||
".rm",
|
||||
".mpa",
|
||||
".wav",
|
||||
".wma",
|
||||
".ogg",
|
||||
".opus",
|
||||
".mp3",
|
||||
".mp2",
|
||||
".mod",
|
||||
".amf",
|
||||
".669",
|
||||
".3gp",
|
||||
".aa",
|
||||
".aac",
|
||||
".aax",
|
||||
".ac3",
|
||||
".act",
|
||||
".adp",
|
||||
".adplug",
|
||||
".adx",
|
||||
".afc",
|
||||
".amf",
|
||||
".aif",
|
||||
".aiff",
|
||||
".alac",
|
||||
".amr",
|
||||
".ape",
|
||||
".ast",
|
||||
".au",
|
||||
".awb",
|
||||
".cda",
|
||||
".cue",
|
||||
".dmf",
|
||||
".dsf",
|
||||
".dsm",
|
||||
".dsp",
|
||||
".dts",
|
||||
".dvf",
|
||||
".far",
|
||||
".flac",
|
||||
".gdm",
|
||||
".gsm",
|
||||
".gym",
|
||||
".hps",
|
||||
".imf",
|
||||
".it",
|
||||
".m15",
|
||||
".m4a",
|
||||
".m4b",
|
||||
".mac",
|
||||
".med",
|
||||
".mka",
|
||||
".mmf",
|
||||
".mod",
|
||||
".mogg",
|
||||
".mp2",
|
||||
".mp3",
|
||||
".mpa",
|
||||
".mpc",
|
||||
".mpp",
|
||||
".mp+",
|
||||
".msv",
|
||||
".nmf",
|
||||
".nsf",
|
||||
".nsv",
|
||||
".oga",
|
||||
".ogg",
|
||||
".okt",
|
||||
".opus",
|
||||
".pls",
|
||||
".ra",
|
||||
".rf64",
|
||||
".rm",
|
||||
".s3m",
|
||||
".stm",
|
||||
".sfx",
|
||||
".shn",
|
||||
".sid",
|
||||
".spc",
|
||||
".stm",
|
||||
".strm",
|
||||
".ult",
|
||||
".uni",
|
||||
".xm",
|
||||
".sid",
|
||||
".ac3",
|
||||
".dts",
|
||||
".cue",
|
||||
".aif",
|
||||
".aiff",
|
||||
".ape",
|
||||
".mac",
|
||||
".mpc",
|
||||
".mp+",
|
||||
".mpp",
|
||||
".shn",
|
||||
".vox",
|
||||
".wav",
|
||||
".wma",
|
||||
".wv",
|
||||
".nsf",
|
||||
".spc",
|
||||
".gym",
|
||||
".adplug",
|
||||
".adx",
|
||||
".dsp",
|
||||
".adp",
|
||||
".ymf",
|
||||
".ast",
|
||||
".afc",
|
||||
".hps",
|
||||
".xm",
|
||||
".xsp",
|
||||
".acc",
|
||||
".m4b",
|
||||
".oga",
|
||||
".dsf",
|
||||
".mka"
|
||||
".ymf"
|
||||
};
|
||||
|
||||
MediaFlagDelimiters = new[]
|
||||
{
|
||||
'.'
|
||||
};
|
||||
|
||||
MediaForcedFlags = new[]
|
||||
{
|
||||
"foreign",
|
||||
"forced"
|
||||
};
|
||||
|
||||
MediaDefaultFlags = new[]
|
||||
{
|
||||
"default"
|
||||
};
|
||||
|
||||
EpisodeExpressions = new[]
|
||||
@@ -253,6 +290,8 @@ namespace Emby.Naming.Common
|
||||
},
|
||||
// <!-- foo.ep01, foo.EP_01 -->
|
||||
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
|
||||
// <!-- foo.E01., foo.e01. -->
|
||||
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
|
||||
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
|
||||
{
|
||||
DateTimeFormats = new[]
|
||||
@@ -275,7 +314,7 @@ namespace Emby.Naming.Common
|
||||
// This isn't a Kodi naming rule, but the expression below causes false positives,
|
||||
// so we make sure this one gets tested first.
|
||||
// "Foo Bar 889"
|
||||
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
|
||||
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
@@ -371,6 +410,20 @@ namespace Emby.Naming.Common
|
||||
IsOptimistic = true,
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// Series and season only expression
|
||||
// "the show/season 1", "the show/s01"
|
||||
new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)\/[Ss](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
// Series and season only expression
|
||||
// "the show S01", "the show season 1"
|
||||
new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)[\. _\-]+[sS](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
};
|
||||
|
||||
EpisodeWithoutSeasonExpressions = new[]
|
||||
@@ -385,6 +438,72 @@ namespace Emby.Naming.Common
|
||||
|
||||
VideoExtraRules = new[]
|
||||
{
|
||||
new ExtraRule(
|
||||
ExtraType.Trailer,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"trailers",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.ThemeVideo,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"backdrops",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.ThemeSong,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"theme-music",
|
||||
MediaType.Audio),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.BehindTheScenes,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"behind the scenes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.DeletedScene,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"deleted scenes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Interview,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"interviews",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Scene,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"scenes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Sample,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"samples",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Clip,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"shorts",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Clip,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"featurettes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Unknown,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"extras",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Trailer,
|
||||
ExtraRuleType.Filename,
|
||||
@@ -499,55 +618,17 @@ namespace Emby.Naming.Common
|
||||
"-short",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.BehindTheScenes,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"behind the scenes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.DeletedScene,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"deleted scenes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Interview,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"interviews",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Scene,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"scenes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Sample,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"samples",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Clip,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"shorts",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Clip,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"featurettes",
|
||||
MediaType.Video),
|
||||
|
||||
new ExtraRule(
|
||||
ExtraType.Unknown,
|
||||
ExtraRuleType.DirectoryName,
|
||||
"extras",
|
||||
MediaType.Video),
|
||||
ExtraRuleType.Suffix,
|
||||
"-extra",
|
||||
MediaType.Video)
|
||||
};
|
||||
|
||||
AllExtrasTypesFolderNames = VideoExtraRules
|
||||
.Where(i => i.RuleType == ExtraRuleType.DirectoryName)
|
||||
.ToDictionary(i => i.Token, i => i.ExtraType, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Format3DRules = new[]
|
||||
{
|
||||
// Kodi rules:
|
||||
@@ -601,41 +682,6 @@ namespace Emby.Naming.Common
|
||||
@"^\s*(?<name>[^ ].*?)\s*$"
|
||||
};
|
||||
|
||||
var extensions = VideoFileExtensions.ToList();
|
||||
|
||||
extensions.AddRange(new[]
|
||||
{
|
||||
".mkv",
|
||||
".m2t",
|
||||
".m2ts",
|
||||
".img",
|
||||
".iso",
|
||||
".mk3d",
|
||||
".ts",
|
||||
".rmvb",
|
||||
".mov",
|
||||
".avi",
|
||||
".mpg",
|
||||
".mpeg",
|
||||
".wmv",
|
||||
".mp4",
|
||||
".divx",
|
||||
".dvr-ms",
|
||||
".wtv",
|
||||
".ogm",
|
||||
".ogv",
|
||||
".asf",
|
||||
".m4v",
|
||||
".flv",
|
||||
".f4v",
|
||||
".3gp",
|
||||
".webm",
|
||||
".mts",
|
||||
".m2v",
|
||||
".rec",
|
||||
".mxf"
|
||||
});
|
||||
|
||||
MultipleEpisodeExpressions = new[]
|
||||
{
|
||||
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
|
||||
@@ -653,18 +699,34 @@ namespace Emby.Naming.Common
|
||||
IsNamed = true
|
||||
}).ToArray();
|
||||
|
||||
VideoFileExtensions = extensions
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
Compile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the folder name to extra types mapping.
|
||||
/// </summary>
|
||||
public Dictionary<string, ExtraType> AllExtrasTypesFolderNames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of audio file extensions.
|
||||
/// </summary>
|
||||
public string[] AudioFileExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of external media flag delimiters.
|
||||
/// </summary>
|
||||
public char[] MediaFlagDelimiters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of external media forced flags.
|
||||
/// </summary>
|
||||
public string[] MediaForcedFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of external media default flags.
|
||||
/// </summary>
|
||||
public string[] MediaDefaultFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of album stacking prefixes.
|
||||
/// </summary>
|
||||
@@ -675,21 +737,6 @@ namespace Emby.Naming.Common
|
||||
/// </summary>
|
||||
public string[] SubtitleFileExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of subtitles flag delimiters.
|
||||
/// </summary>
|
||||
public char[] SubtitleFlagDelimiters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of subtitle forced flags.
|
||||
/// </summary>
|
||||
public string[] SubtitleForcedFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of subtitle default flags.
|
||||
/// </summary>
|
||||
public string[] SubtitleDefaultFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of episode regular expressions.
|
||||
/// </summary>
|
||||
@@ -741,9 +788,9 @@ namespace Emby.Naming.Common
|
||||
public Format3DRule[] Format3DRules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of raw video file-stacking expressions strings.
|
||||
/// Gets the file stacking rules.
|
||||
/// </summary>
|
||||
public string[] VideoFileStackingExpressions { get; set; }
|
||||
public FileStackRule[] VideoFileStackingRules { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of raw clean DateTimes regular expressions strings.
|
||||
@@ -765,11 +812,6 @@ namespace Emby.Naming.Common
|
||||
/// </summary>
|
||||
public ExtraRule[] VideoExtraRules { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets list of video file-stack regular expressions.
|
||||
/// </summary>
|
||||
public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets list of clean datetime regular expressions.
|
||||
/// </summary>
|
||||
@@ -795,7 +837,6 @@ namespace Emby.Naming.Common
|
||||
/// </summary>
|
||||
public void Compile()
|
||||
{
|
||||
VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
|
||||
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
|
||||
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
|
||||
EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
||||
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
@@ -32,19 +36,23 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.8.0</VersionPrefix>
|
||||
<VersionPrefix>10.8.8</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
116
Emby.Naming/ExternalFiles/ExternalPathParser.cs
Normal file
116
Emby.Naming/ExternalFiles/ExternalPathParser.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace Emby.Naming.ExternalFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// External media file parser class.
|
||||
/// </summary>
|
||||
public class ExternalPathParser
|
||||
{
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly DlnaProfileType _type;
|
||||
private readonly ILocalizationManager _localizationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExternalPathParser"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localizationManager">The localization manager.</param>
|
||||
/// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
|
||||
/// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
|
||||
public ExternalPathParser(NamingOptions namingOptions, ILocalizationManager localizationManager, DlnaProfileType type)
|
||||
{
|
||||
_localizationManager = localizationManager;
|
||||
_namingOptions = namingOptions;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse filename and extract information.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to file.</param>
|
||||
/// <param name="extraString">Part of the filename only containing the extra information.</param>
|
||||
/// <returns>Returns null or an <see cref="ExternalPathParserResult"/> object if parsing is successful.</returns>
|
||||
public ExternalPathParserResult? ParseFile(string path, string? extraString)
|
||||
{
|
||||
if (path.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var pathInfo = new ExternalPathParserResult(path);
|
||||
|
||||
if (string.IsNullOrEmpty(extraString))
|
||||
{
|
||||
return pathInfo;
|
||||
}
|
||||
|
||||
foreach (var separator in _namingOptions.MediaFlagDelimiters)
|
||||
{
|
||||
var languageString = extraString;
|
||||
var titleString = string.Empty;
|
||||
const int SeparatorLength = 1;
|
||||
|
||||
while (languageString.Length > 0)
|
||||
{
|
||||
int lastSeparator = languageString.LastIndexOf(separator);
|
||||
|
||||
if (lastSeparator == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
string currentSlice = languageString[lastSeparator..];
|
||||
string currentSliceWithoutSeparator = currentSlice[SeparatorLength..];
|
||||
|
||||
if (_namingOptions.MediaDefaultFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
pathInfo.IsDefault = true;
|
||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
languageString = languageString[..lastSeparator];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_namingOptions.MediaForcedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
pathInfo.IsForced = true;
|
||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
languageString = languageString[..lastSeparator];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to translate to three character code
|
||||
var culture = _localizationManager.FindLanguageInfo(currentSliceWithoutSeparator);
|
||||
|
||||
if (culture != null && pathInfo.Language == null)
|
||||
{
|
||||
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
titleString = currentSlice + titleString;
|
||||
}
|
||||
|
||||
languageString = languageString[..lastSeparator];
|
||||
}
|
||||
|
||||
pathInfo.Title = titleString.Length >= SeparatorLength ? titleString[SeparatorLength..] : null;
|
||||
}
|
||||
|
||||
return pathInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
namespace Emby.Naming.Subtitles
|
||||
namespace Emby.Naming.ExternalFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Class holding information about subtitle.
|
||||
/// Class holding information about external files.
|
||||
/// </summary>
|
||||
public class SubtitleInfo
|
||||
public class ExternalPathParserResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubtitleInfo"/> class.
|
||||
/// Initializes a new instance of the <see cref="ExternalPathParserResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to file.</param>
|
||||
/// <param name="isDefault">Is subtitle default.</param>
|
||||
/// <param name="isForced">Is subtitle forced.</param>
|
||||
public SubtitleInfo(string path, bool isDefault, bool isForced)
|
||||
/// <param name="isDefault">Is default.</param>
|
||||
/// <param name="isForced">Is forced.</param>
|
||||
public ExternalPathParserResult(string path, bool isDefault = false, bool isForced = false)
|
||||
{
|
||||
Path = path;
|
||||
IsDefault = isDefault;
|
||||
@@ -30,6 +30,12 @@ namespace Emby.Naming.Subtitles
|
||||
/// <value>The language.</value>
|
||||
public string? Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
/// <value>The title.</value>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is default.
|
||||
/// </summary>
|
||||
@@ -1,70 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
|
||||
namespace Emby.Naming.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// Subtitle Parser class.
|
||||
/// </summary>
|
||||
public class SubtitleParser
|
||||
{
|
||||
private readonly NamingOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubtitleParser"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object containing SubtitleFileExtensions, SubtitleDefaultFlags, SubtitleForcedFlags and SubtitleFlagDelimiters.</param>
|
||||
public SubtitleParser(NamingOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse file to determine if is subtitle and <see cref="SubtitleInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to file.</param>
|
||||
/// <returns>Returns null or <see cref="SubtitleInfo"/> object if parsing is successful.</returns>
|
||||
public SubtitleInfo? ParseFile(string path)
|
||||
{
|
||||
if (path.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var flags = GetFlags(path);
|
||||
var info = new SubtitleInfo(
|
||||
path,
|
||||
_options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
|
||||
_options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
|
||||
|
||||
var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
|
||||
&& !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
// Should have a name, language and file extension
|
||||
if (parts.Count >= 3)
|
||||
{
|
||||
info.Language = parts[^2];
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private string[] GetFlags(string path)
|
||||
{
|
||||
// Note: the tags need be surrounded be either a space ( ), hyphen -, dot . or underscore _.
|
||||
|
||||
var file = Path.GetFileName(path);
|
||||
|
||||
return file.Split(_options.SubtitleFlagDelimiters, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
@@ -48,7 +48,7 @@ namespace Emby.Naming.TV
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
// Check supported extensions
|
||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// It's not supported. Check stub extensions
|
||||
if (!StubResolver.TryResolveFile(path, _options, out stubType))
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Emby.Naming.TV
|
||||
/// <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>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPath(
|
||||
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
|
||||
string path,
|
||||
bool supportSpecialAliases,
|
||||
bool supportNumericSeasonFolders)
|
||||
@@ -99,7 +99,7 @@ namespace Emby.Naming.TV
|
||||
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
|
||||
if (result.seasonNumber.HasValue)
|
||||
if (result.SeasonNumber.HasValue)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
@@ -142,7 +142,7 @@ namespace Emby.Naming.TV
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
|
||||
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
|
||||
{
|
||||
var numericStart = -1;
|
||||
var length = 0;
|
||||
|
||||
29
Emby.Naming/TV/SeriesInfo.cs
Normal file
29
Emby.Naming/TV/SeriesInfo.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Holder object for Series information.
|
||||
/// </summary>
|
||||
public class SeriesInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeriesInfo"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the file.</param>
|
||||
public SeriesInfo(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path.
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the series.
|
||||
/// </summary>
|
||||
/// <value>The name of the series.</value>
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
}
|
||||
60
Emby.Naming/TV/SeriesPathParser.cs
Normal file
60
Emby.Naming/TV/SeriesPathParser.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Emby.Naming.Common;
|
||||
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to parse information about series from paths containing more information that only the series name.
|
||||
/// Uses the same regular expressions as the EpisodePathParser but have different success criteria.
|
||||
/// </summary>
|
||||
public static class SeriesPathParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses information about series from path.
|
||||
/// </summary>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
|
||||
/// <param name="path">Path.</param>
|
||||
/// <returns>Returns <see cref="SeriesPathParserResult"/> object.</returns>
|
||||
public static SeriesPathParserResult Parse(NamingOptions options, string path)
|
||||
{
|
||||
SeriesPathParserResult? result = null;
|
||||
|
||||
foreach (var expression in options.EpisodeExpressions)
|
||||
{
|
||||
var currentResult = Parse(path, expression);
|
||||
if (currentResult.Success)
|
||||
{
|
||||
result = currentResult;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(result.SeriesName))
|
||||
{
|
||||
result.SeriesName = result.SeriesName.Trim(' ', '_', '.', '-');
|
||||
}
|
||||
}
|
||||
|
||||
return result ?? new SeriesPathParserResult();
|
||||
}
|
||||
|
||||
private static SeriesPathParserResult Parse(string name, EpisodeExpression expression)
|
||||
{
|
||||
var result = new SeriesPathParserResult();
|
||||
|
||||
var match = expression.Regex.Match(name);
|
||||
|
||||
if (match.Success && match.Groups.Count >= 3)
|
||||
{
|
||||
if (expression.IsNamed)
|
||||
{
|
||||
result.SeriesName = match.Groups["seriesname"].Value;
|
||||
result.Success = !string.IsNullOrEmpty(result.SeriesName) && !match.Groups["seasonnumber"].ValueSpan.IsEmpty;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Emby.Naming/TV/SeriesPathParserResult.cs
Normal file
19
Emby.Naming/TV/SeriesPathParserResult.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Holder object for <see cref="SeriesPathParser"/> result.
|
||||
/// </summary>
|
||||
public class SeriesPathParserResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the series.
|
||||
/// </summary>
|
||||
/// <value>The name of the series.</value>
|
||||
public string? SeriesName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether parsing was successful.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
}
|
||||
49
Emby.Naming/TV/SeriesResolver.cs
Normal file
49
Emby.Naming/TV/SeriesResolver.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Common;
|
||||
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to resolve information about series from path.
|
||||
/// </summary>
|
||||
public static class SeriesResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// preserving namings like "S.H.O.W".
|
||||
/// </summary>
|
||||
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))");
|
||||
|
||||
/// <summary>
|
||||
/// Resolve information about series from path.
|
||||
/// </summary>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object passed to <see cref="SeriesPathParser"/>.</param>
|
||||
/// <param name="path">Path to series.</param>
|
||||
/// <returns>SeriesInfo.</returns>
|
||||
public static SeriesInfo Resolve(NamingOptions options, string path)
|
||||
{
|
||||
string seriesName = Path.GetFileName(path);
|
||||
|
||||
SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
|
||||
if (result.Success)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(result.SeriesName))
|
||||
{
|
||||
seriesName = result.SeriesName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(seriesName))
|
||||
{
|
||||
seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
|
||||
}
|
||||
|
||||
return new SeriesInfo(path)
|
||||
{
|
||||
Name = seriesName
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -9,45 +9,27 @@ namespace Emby.Naming.Video
|
||||
/// <summary>
|
||||
/// Resolve if file is extra for video.
|
||||
/// </summary>
|
||||
public class ExtraResolver
|
||||
public static class ExtraRuleResolver
|
||||
{
|
||||
private static readonly char[] _digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
|
||||
private readonly NamingOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExtraResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
|
||||
public ExtraResolver(NamingOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve if file is extra.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to file.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||
public ExtraResult GetExtraInfo(string path)
|
||||
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
|
||||
{
|
||||
var result = new ExtraResult();
|
||||
|
||||
for (var i = 0; i < _options.VideoExtraRules.Length; i++)
|
||||
for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
|
||||
{
|
||||
var rule = _options.VideoExtraRules[i];
|
||||
if (rule.MediaType == MediaType.Audio)
|
||||
var rule = namingOptions.VideoExtraRules[i];
|
||||
if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
|
||||
|| (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
|
||||
{
|
||||
if (!AudioFileParser.IsAudioFile(path, _options))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (rule.MediaType == MediaType.Video)
|
||||
{
|
||||
if (!VideoResolver.IsVideoFile(path, _options))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var pathSpan = path.AsSpan();
|
||||
@@ -76,9 +58,9 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
var filename = Path.GetFileName(path);
|
||||
|
||||
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
|
||||
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
if (regex.IsMatch(filename))
|
||||
if (isMatch)
|
||||
{
|
||||
result.ExtraType = rule.ExtraType;
|
||||
result.Rule = rule;
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
{
|
||||
@@ -12,25 +12,30 @@ namespace Emby.Naming.Video
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileStack"/> class.
|
||||
/// </summary>
|
||||
public FileStack()
|
||||
/// <param name="name">The stack name.</param>
|
||||
/// <param name="isDirectory">Whether the stack files are directories.</param>
|
||||
/// <param name="files">The stack files.</param>
|
||||
public FileStack(string name, bool isDirectory, IReadOnlyList<string> files)
|
||||
{
|
||||
Files = new List<string>();
|
||||
Name = name;
|
||||
IsDirectoryStack = isDirectory;
|
||||
Files = files;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets name of file stack.
|
||||
/// Gets the name of file stack.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of paths in stack.
|
||||
/// Gets the list of paths in stack.
|
||||
/// </summary>
|
||||
public List<string> Files { get; set; }
|
||||
public IReadOnlyList<string> Files { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether stack is directory stack.
|
||||
/// Gets a value indicating whether stack is directory stack.
|
||||
/// </summary>
|
||||
public bool IsDirectoryStack { get; set; }
|
||||
public bool IsDirectoryStack { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to determine if path is in the stack.
|
||||
@@ -40,12 +45,12 @@ namespace Emby.Naming.Video
|
||||
/// <returns>True if file is in the stack.</returns>
|
||||
public bool ContainsFile(string file, bool isDirectory)
|
||||
{
|
||||
if (IsDirectoryStack == isDirectory)
|
||||
if (string.IsNullOrEmpty(file))
|
||||
{
|
||||
return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
Emby.Naming/Video/FileStackRule.cs
Normal file
48
Emby.Naming/Video/FileStackRule.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Emby.Naming.Video;
|
||||
|
||||
/// <summary>
|
||||
/// Regex based rule for file stacking (eg. disc1, disc2).
|
||||
/// </summary>
|
||||
public class FileStackRule
|
||||
{
|
||||
private readonly Regex _tokenRegex;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileStackRule"/> class.
|
||||
/// </summary>
|
||||
/// <param name="token">Token.</param>
|
||||
/// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
|
||||
public FileStackRule(string token, bool isNumerical)
|
||||
{
|
||||
_tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
|
||||
IsNumerical = isNumerical;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
|
||||
/// </summary>
|
||||
public bool IsNumerical { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Match the input against the rule regex.
|
||||
/// </summary>
|
||||
/// <param name="input">The input.</param>
|
||||
/// <param name="result">The part type and number or <c>null</c>.</param>
|
||||
/// <returns>A value indicating whether the input matched the rule.</returns>
|
||||
public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result)
|
||||
{
|
||||
result = null;
|
||||
var match = _tokenRegex.Match(input);
|
||||
if (!match.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "unknown";
|
||||
result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Emby.Naming.Video
|
||||
public static class Format3DParser
|
||||
{
|
||||
// Static default result to save on allocation costs.
|
||||
private static readonly Format3DResult _defaultResult = new (false, null);
|
||||
private static readonly Format3DResult _defaultResult = new(false, null);
|
||||
|
||||
/// <summary>
|
||||
/// Parse 3D format related flags.
|
||||
|
||||
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.AudioBook;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -12,37 +11,28 @@ namespace Emby.Naming.Video
|
||||
/// <summary>
|
||||
/// Resolve <see cref="FileStack"/> from list of paths.
|
||||
/// </summary>
|
||||
public class StackResolver
|
||||
public static class StackResolver
|
||||
{
|
||||
private readonly NamingOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StackResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
|
||||
public StackResolver(NamingOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves only directories from paths.
|
||||
/// </summary>
|
||||
/// <param name="files">List of paths.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
|
||||
public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
|
||||
public static IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files, NamingOptions namingOptions)
|
||||
{
|
||||
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
|
||||
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves only files from paths.
|
||||
/// </summary>
|
||||
/// <param name="files">List of paths.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
|
||||
public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
|
||||
public static IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files, NamingOptions namingOptions)
|
||||
{
|
||||
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
|
||||
return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -50,7 +40,7 @@ namespace Emby.Naming.Video
|
||||
/// </summary>
|
||||
/// <param name="files">List of paths.</param>
|
||||
/// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
|
||||
public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
|
||||
public static IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
|
||||
{
|
||||
var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
|
||||
|
||||
@@ -60,19 +50,13 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
foreach (var file in directory)
|
||||
{
|
||||
var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
|
||||
stack.Files.Add(file.Path);
|
||||
var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
|
||||
yield return stack;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
|
||||
foreach (var file in directory)
|
||||
{
|
||||
stack.Files.Add(file.Path);
|
||||
}
|
||||
|
||||
var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
|
||||
yield return stack;
|
||||
}
|
||||
}
|
||||
@@ -82,158 +66,91 @@ namespace Emby.Naming.Video
|
||||
/// Resolves videos from paths.
|
||||
/// </summary>
|
||||
/// <param name="files">List of paths.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
||||
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
|
||||
public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions)
|
||||
{
|
||||
var list = files
|
||||
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
|
||||
.OrderBy(i => i.FullName)
|
||||
.ToList();
|
||||
var potentialFiles = files
|
||||
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
|
||||
.OrderBy(i => i.FullName);
|
||||
|
||||
var expressions = _options.VideoFileStackingRegexes;
|
||||
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
var potentialStacks = new Dictionary<string, StackMetadata>();
|
||||
foreach (var file in potentialFiles)
|
||||
{
|
||||
var offset = 0;
|
||||
|
||||
var file1 = list[i];
|
||||
|
||||
var expressionIndex = 0;
|
||||
while (expressionIndex < expressions.Length)
|
||||
var name = file.Name;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
var exp = expressions[expressionIndex];
|
||||
var stack = new FileStack();
|
||||
name = Path.GetFileName(file.FullName);
|
||||
}
|
||||
|
||||
// (Title)(Volume)(Ignore)(Extension)
|
||||
var match1 = FindMatch(file1, exp, offset);
|
||||
|
||||
if (match1.Success)
|
||||
for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
|
||||
{
|
||||
var rule = namingOptions.VideoFileStackingRules[i];
|
||||
if (!rule.Match(name, out var stackParsingResult))
|
||||
{
|
||||
var title1 = match1.Groups["title"].Value;
|
||||
var volume1 = match1.Groups["volume"].Value;
|
||||
var ignore1 = match1.Groups["ignore"].Value;
|
||||
var extension1 = match1.Groups["extension"].Value;
|
||||
continue;
|
||||
}
|
||||
|
||||
var j = i + 1;
|
||||
while (j < list.Count)
|
||||
var stackName = stackParsingResult.Value.StackName;
|
||||
var partNumber = stackParsingResult.Value.PartNumber;
|
||||
var partType = stackParsingResult.Value.PartType;
|
||||
|
||||
if (!potentialStacks.TryGetValue(stackName, out var stackResult))
|
||||
{
|
||||
stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
|
||||
potentialStacks[stackName] = stackResult;
|
||||
}
|
||||
|
||||
if (stackResult.Parts.Count > 0)
|
||||
{
|
||||
if (stackResult.IsDirectory != file.IsDirectory
|
||||
|| !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
|
||||
|| stackResult.ContainsPart(partNumber))
|
||||
{
|
||||
var file2 = list[j];
|
||||
|
||||
if (file1.IsDirectory != file2.IsDirectory)
|
||||
{
|
||||
j++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// (Title)(Volume)(Ignore)(Extension)
|
||||
var match2 = FindMatch(file2, exp, offset);
|
||||
|
||||
if (match2.Success)
|
||||
{
|
||||
var title2 = match2.Groups[1].Value;
|
||||
var volume2 = match2.Groups[2].Value;
|
||||
var ignore2 = match2.Groups[3].Value;
|
||||
var extension2 = match2.Groups[4].Value;
|
||||
|
||||
if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (stack.Files.Count == 0)
|
||||
{
|
||||
stack.Name = title1 + ignore1;
|
||||
stack.IsDirectoryStack = file1.IsDirectory;
|
||||
stack.Files.Add(file1.FullName);
|
||||
}
|
||||
|
||||
stack.Files.Add(file2.FullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Sequel
|
||||
offset = 0;
|
||||
expressionIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// False positive, try again with offset
|
||||
offset = match1.Groups[3].Index;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Extension mismatch
|
||||
offset = 0;
|
||||
expressionIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Title mismatch
|
||||
offset = 0;
|
||||
expressionIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No match 2, next expression
|
||||
offset = 0;
|
||||
expressionIndex++;
|
||||
break;
|
||||
}
|
||||
|
||||
j++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (j == list.Count)
|
||||
if (rule.IsNumerical != stackResult.IsNumerical)
|
||||
{
|
||||
expressionIndex = expressions.Length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No match 1
|
||||
offset = 0;
|
||||
expressionIndex++;
|
||||
}
|
||||
|
||||
if (stack.Files.Count > 1)
|
||||
{
|
||||
yield return stack;
|
||||
i += stack.Files.Count - 1;
|
||||
break;
|
||||
}
|
||||
stackResult.Parts.Add(partNumber, file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRegexInput(FileSystemMetadata file)
|
||||
{
|
||||
// For directories, dummy up an extension otherwise the expressions will fail
|
||||
var input = !file.IsDirectory
|
||||
? file.FullName
|
||||
: file.FullName + ".mkv";
|
||||
|
||||
return Path.GetFileName(input);
|
||||
}
|
||||
|
||||
private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
|
||||
{
|
||||
var regexInput = GetRegexInput(input);
|
||||
|
||||
if (offset < 0 || offset >= regexInput.Length)
|
||||
foreach (var (fileName, stack) in potentialStacks)
|
||||
{
|
||||
return Match.Empty;
|
||||
if (stack.Parts.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private class StackMetadata
|
||||
{
|
||||
public StackMetadata(bool isDirectory, bool isNumerical, string partType)
|
||||
{
|
||||
Parts = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
|
||||
IsDirectory = isDirectory;
|
||||
IsNumerical = isNumerical;
|
||||
PartType = partType;
|
||||
}
|
||||
|
||||
return regex.Match(regexInput, offset);
|
||||
public Dictionary<string, FileSystemMetadata> Parts { get; }
|
||||
|
||||
public bool IsDirectory { get; }
|
||||
|
||||
public bool IsNumerical { get; }
|
||||
|
||||
public string PartType { get; }
|
||||
|
||||
public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
{
|
||||
@@ -28,7 +28,7 @@ namespace Emby.Naming.Video
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
|
||||
if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
{
|
||||
@@ -17,7 +18,6 @@ namespace Emby.Naming.Video
|
||||
Name = name;
|
||||
|
||||
Files = Array.Empty<VideoFileInfo>();
|
||||
Extras = Array.Empty<VideoFileInfo>();
|
||||
AlternateVersions = Array.Empty<VideoFileInfo>();
|
||||
}
|
||||
|
||||
@@ -39,16 +39,15 @@ namespace Emby.Naming.Video
|
||||
/// <value>The files.</value>
|
||||
public IReadOnlyList<VideoFileInfo> Files { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extras.
|
||||
/// </summary>
|
||||
/// <value>The extras.</value>
|
||||
public IReadOnlyList<VideoFileInfo> Extras { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alternate versions.
|
||||
/// </summary>
|
||||
/// <value>The alternate versions.</value>
|
||||
public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extra type.
|
||||
/// </summary>
|
||||
public ExtraType? ExtraType { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
@@ -17,29 +16,41 @@ namespace Emby.Naming.Video
|
||||
/// <summary>
|
||||
/// Resolves alternative versions and extras from list of video files.
|
||||
/// </summary>
|
||||
/// <param name="files">List of related video files.</param>
|
||||
/// <param name="videoInfos">List of related video files.</param>
|
||||
/// <param name="namingOptions">The naming options.</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>
|
||||
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
||||
public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
|
||||
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
|
||||
{
|
||||
var videoInfos = files
|
||||
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
|
||||
.OfType<VideoFileInfo>()
|
||||
.ToList();
|
||||
|
||||
// Filter out all extras, otherwise they could cause stacks to not be resolved
|
||||
// See the unit test TestStackedWithTrailer
|
||||
var nonExtras = videoInfos
|
||||
.Where(i => i.ExtraType == null)
|
||||
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
|
||||
|
||||
var stackResult = new StackResolver(namingOptions)
|
||||
.Resolve(nonExtras).ToList();
|
||||
var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
|
||||
|
||||
var remainingFiles = videoInfos
|
||||
.Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
|
||||
.ToList();
|
||||
var remainingFiles = new List<VideoFileInfo>();
|
||||
var standaloneMedia = new List<VideoFileInfo>();
|
||||
|
||||
for (var i = 0; i < videoInfos.Count; i++)
|
||||
{
|
||||
var current = videoInfos[i];
|
||||
if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.ExtraType == null)
|
||||
{
|
||||
standaloneMedia.Add(current);
|
||||
}
|
||||
else
|
||||
{
|
||||
remainingFiles.Add(current);
|
||||
}
|
||||
}
|
||||
|
||||
var list = new List<VideoInfo>();
|
||||
|
||||
@@ -47,38 +58,20 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
var info = new VideoInfo(stack.Name)
|
||||
{
|
||||
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
|
||||
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
|
||||
.OfType<VideoFileInfo>()
|
||||
.ToList()
|
||||
};
|
||||
|
||||
info.Year = info.Files[0].Year;
|
||||
|
||||
var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
|
||||
|
||||
if (extras.Count > 0)
|
||||
{
|
||||
info.Extras = extras;
|
||||
}
|
||||
|
||||
list.Add(info);
|
||||
}
|
||||
|
||||
var standaloneMedia = remainingFiles
|
||||
.Where(i => i.ExtraType == null)
|
||||
.ToList();
|
||||
|
||||
foreach (var media in standaloneMedia)
|
||||
{
|
||||
var info = new VideoInfo(media.Name) { Files = new[] { media } };
|
||||
|
||||
info.Year = info.Files[0].Year;
|
||||
|
||||
remainingFiles.Remove(media);
|
||||
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
|
||||
|
||||
info.Extras = extras;
|
||||
|
||||
list.Add(info);
|
||||
}
|
||||
|
||||
@@ -87,58 +80,12 @@ namespace Emby.Naming.Video
|
||||
list = GetVideosGroupedByVersion(list, namingOptions);
|
||||
}
|
||||
|
||||
// If there's only one resolved video, use the folder name as well to find extras
|
||||
if (list.Count == 1)
|
||||
{
|
||||
var info = list[0];
|
||||
var videoPath = list[0].Files[0].Path;
|
||||
var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
|
||||
|
||||
if (!parentPath.IsEmpty)
|
||||
{
|
||||
var folderName = Path.GetFileName(parentPath);
|
||||
if (!folderName.IsEmpty)
|
||||
{
|
||||
var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
|
||||
extras.AddRange(info.Extras);
|
||||
info.Extras = extras;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the extras that are just based on file name as well
|
||||
var extrasByFileName = remainingFiles
|
||||
.Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
|
||||
.ToList();
|
||||
|
||||
remainingFiles = remainingFiles
|
||||
.Except(extrasByFileName)
|
||||
.ToList();
|
||||
|
||||
extrasByFileName.AddRange(info.Extras);
|
||||
info.Extras = extrasByFileName;
|
||||
}
|
||||
|
||||
// If there's only one video, accept all trailers
|
||||
// Be lenient because people use all kinds of mishmash conventions with trailers.
|
||||
if (list.Count == 1)
|
||||
{
|
||||
var trailers = remainingFiles
|
||||
.Where(i => i.ExtraType == ExtraType.Trailer)
|
||||
.ToList();
|
||||
|
||||
trailers.AddRange(list[0].Extras);
|
||||
list[0].Extras = trailers;
|
||||
|
||||
remainingFiles = remainingFiles
|
||||
.Except(trailers)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Whatever files are left, just add them
|
||||
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
|
||||
{
|
||||
Files = new[] { i },
|
||||
Year = i.Year
|
||||
Year = i.Year,
|
||||
ExtraType = i.ExtraType
|
||||
}));
|
||||
|
||||
return list;
|
||||
@@ -162,6 +109,11 @@ namespace Emby.Naming.Video
|
||||
for (var i = 0; i < videos.Count; i++)
|
||||
{
|
||||
var video = videos[i];
|
||||
if (video.ExtraType != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
|
||||
{
|
||||
return videos;
|
||||
@@ -178,17 +130,14 @@ namespace Emby.Naming.Video
|
||||
|
||||
var alternateVersionsLen = videos.Count - 1;
|
||||
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
||||
var extras = new List<VideoFileInfo>(list[0].Extras);
|
||||
for (int i = 0; i < alternateVersionsLen; i++)
|
||||
{
|
||||
var video = videos[i + 1];
|
||||
alternateVersions[i] = video.Files[0];
|
||||
extras.AddRange(video.Extras);
|
||||
}
|
||||
|
||||
list[0].AlternateVersions = alternateVersions;
|
||||
list[0].Name = folderName.ToString();
|
||||
list[0].Extras = extras;
|
||||
|
||||
return list;
|
||||
}
|
||||
@@ -230,7 +179,7 @@ namespace Emby.Naming.Video
|
||||
var tmpTestFilename = testFilename.ToString();
|
||||
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
|
||||
{
|
||||
tmpTestFilename = cleanName.Trim().ToString();
|
||||
tmpTestFilename = cleanName.Trim();
|
||||
}
|
||||
|
||||
// The CleanStringParser should have removed common keywords etc.
|
||||
@@ -238,67 +187,5 @@ namespace Emby.Naming.Video
|
||||
|| testFilename[0] == '-'
|
||||
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
|
||||
{
|
||||
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
|
||||
}
|
||||
|
||||
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
|
||||
{
|
||||
if (baseName.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
|
||||
|| (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
|
||||
/// </summary>
|
||||
/// <param name="remainingFiles">The list of remaining filenames.</param>
|
||||
/// <param name="baseName">The base name to use for the comparison.</param>
|
||||
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
|
||||
/// <returns>A list of video extras for [baseName].</returns>
|
||||
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
|
||||
{
|
||||
return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
|
||||
/// </summary>
|
||||
/// <param name="remainingFiles">The list of remaining filenames.</param>
|
||||
/// <param name="firstBaseName">The first base name to use for the comparison.</param>
|
||||
/// <param name="secondBaseName">The second base name to use for the comparison.</param>
|
||||
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
|
||||
/// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
|
||||
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
|
||||
{
|
||||
var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
|
||||
var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
|
||||
|
||||
var result = new List<VideoFileInfo>();
|
||||
for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
|
||||
{
|
||||
var file = remainingFiles[pos];
|
||||
if (file.ExtraType == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var filename = file.FileNameWithoutExtension;
|
||||
if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
|
||||
|| StartsWith(filename, secondBaseName, trimmedSecondBaseName))
|
||||
{
|
||||
result.Add(file);
|
||||
remainingFiles.RemoveAt(pos);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ namespace Emby.Naming.Video
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="parseName">Whether to parse the name or use the filename.</param>
|
||||
/// <returns>VideoFileInfo.</returns>
|
||||
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
|
||||
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
|
||||
{
|
||||
return Resolve(path, true, namingOptions);
|
||||
return Resolve(path, true, namingOptions, parseName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,7 +75,7 @@ namespace Emby.Naming.Video
|
||||
|
||||
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
||||
|
||||
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
|
||||
var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
|
||||
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
|
||||
@@ -24,63 +24,63 @@ namespace Emby.Notifications
|
||||
{
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.ApplicationUpdateInstalled.ToString()
|
||||
Type = nameof(NotificationType.ApplicationUpdateInstalled)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.InstallationFailed.ToString()
|
||||
Type = nameof(NotificationType.InstallationFailed)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.PluginInstalled.ToString()
|
||||
Type = nameof(NotificationType.PluginInstalled)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.PluginError.ToString()
|
||||
Type = nameof(NotificationType.PluginError)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.PluginUninstalled.ToString()
|
||||
Type = nameof(NotificationType.PluginUninstalled)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.PluginUpdateInstalled.ToString()
|
||||
Type = nameof(NotificationType.PluginUpdateInstalled)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.ServerRestartRequired.ToString()
|
||||
Type = nameof(NotificationType.ServerRestartRequired)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.TaskFailed.ToString()
|
||||
Type = nameof(NotificationType.TaskFailed)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.NewLibraryContent.ToString()
|
||||
Type = nameof(NotificationType.NewLibraryContent)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.AudioPlayback.ToString()
|
||||
Type = nameof(NotificationType.AudioPlayback)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.VideoPlayback.ToString()
|
||||
Type = nameof(NotificationType.VideoPlayback)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.AudioPlaybackStopped.ToString()
|
||||
Type = nameof(NotificationType.AudioPlaybackStopped)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.VideoPlaybackStopped.ToString()
|
||||
Type = nameof(NotificationType.VideoPlaybackStopped)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.UserLockedOut.ToString()
|
||||
Type = nameof(NotificationType.UserLockedOut)
|
||||
},
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.ApplicationUpdateAvailable.ToString()
|
||||
Type = nameof(NotificationType.ApplicationUpdateAvailable)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace Emby.Notifications
|
||||
|
||||
private void Update(NotificationTypeInfo note)
|
||||
{
|
||||
note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type) ?? note.Type;
|
||||
note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type);
|
||||
|
||||
note.IsBasedOnUserEvent = note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
|
||||
|
||||
@@ -23,8 +23,12 @@
|
||||
|
||||
<!-- Code analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -104,14 +105,14 @@ namespace Emby.Notifications
|
||||
|
||||
var type = entry.Type;
|
||||
|
||||
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
|
||||
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = e.Argument.UserId;
|
||||
|
||||
if (!userId.Equals(Guid.Empty) && !GetOptions().IsEnabledToMonitorUser(type, userId))
|
||||
if (!userId.Equals(default) && !GetOptions().IsEnabledToMonitorUser(type, userId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="TagLibSharp" Version="2.2.0" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -26,7 +26,11 @@
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -60,7 +61,7 @@ namespace Emby.Photos
|
||||
item.SetImagePath(ImageType.Primary, item.Path);
|
||||
|
||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
|
||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -301,7 +301,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
{
|
||||
return _configurations.GetOrAdd(
|
||||
key,
|
||||
(k, configurationManager) =>
|
||||
static (k, configurationManager) =>
|
||||
{
|
||||
var file = configurationManager.GetConfigurationFile(k);
|
||||
|
||||
@@ -371,7 +371,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
NewConfiguration = configuration
|
||||
});
|
||||
|
||||
_configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
|
||||
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
|
||||
|
||||
var path = GetConfigurationFile(key);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
@@ -398,6 +398,12 @@ namespace Emby.Server.Implementations.AppBase
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConfigurationStore[] GetConfigurationStores()
|
||||
{
|
||||
return _configurationStores;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type GetConfigurationType(string key)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.AppBase
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
@@ -18,6 +19,7 @@ using Emby.Dlna;
|
||||
using Emby.Dlna.Main;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using Emby.Drawing;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Notifications;
|
||||
using Emby.Photos;
|
||||
using Emby.Server.Implementations.Archiving;
|
||||
@@ -42,9 +44,9 @@ using Emby.Server.Implementations.Serialization;
|
||||
using Emby.Server.Implementations.Session;
|
||||
using Emby.Server.Implementations.SyncPlay;
|
||||
using Emby.Server.Implementations.TV;
|
||||
using Emby.Server.Implementations.Udp;
|
||||
using Emby.Server.Implementations.Updates;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.MediaEncoding.Hls.Playlist;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
using Jellyfin.Networking.Manager;
|
||||
using MediaBrowser.Common;
|
||||
@@ -56,6 +58,7 @@ using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.ClientEvent;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
@@ -100,6 +103,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Prometheus.DotNetRuntime;
|
||||
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
||||
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
|
||||
|
||||
namespace Emby.Server.Implementations
|
||||
@@ -107,7 +111,7 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// Class CompositionRoot.
|
||||
/// </summary>
|
||||
public abstract class ApplicationHost : IServerApplicationHost, IDisposable
|
||||
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The environment variable prefixes to log at server startup.
|
||||
@@ -117,7 +121,7 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// The disposable parts.
|
||||
/// </summary>
|
||||
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
||||
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
|
||||
|
||||
private readonly IFileSystem _fileSystemManager;
|
||||
private readonly IConfiguration _startupConfig;
|
||||
@@ -128,7 +132,6 @@ namespace Emby.Server.Implementations
|
||||
private List<Type> _creatingInstances;
|
||||
private IMediaEncoder _mediaEncoder;
|
||||
private ISessionManager _sessionManager;
|
||||
private string[] _urlPrefixes;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets all concrete types.
|
||||
@@ -147,25 +150,20 @@ namespace Emby.Server.Implementations
|
||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
|
||||
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||
public ApplicationHost(
|
||||
protected ApplicationHost(
|
||||
IServerApplicationPaths applicationPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IStartupOptions options,
|
||||
IConfiguration startupConfig,
|
||||
IFileSystem fileSystem,
|
||||
IServiceCollection serviceCollection)
|
||||
IConfiguration startupConfig)
|
||||
{
|
||||
ApplicationPaths = applicationPaths;
|
||||
LoggerFactory = loggerFactory;
|
||||
_startupOptions = options;
|
||||
_startupConfig = startupConfig;
|
||||
_fileSystemManager = fileSystem;
|
||||
ServiceCollection = serviceCollection;
|
||||
_fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
|
||||
|
||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
||||
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
|
||||
|
||||
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||
@@ -186,6 +184,11 @@ namespace Emby.Server.Implementations
|
||||
/// </summary>
|
||||
public event EventHandler HasPendingRestartChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the PublishedServerUrl setting.
|
||||
/// </summary>
|
||||
private string PublishedServerUrl => _startupConfig[AddressOverrideKey];
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance can self restart.
|
||||
/// </summary>
|
||||
@@ -214,7 +217,7 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
||||
/// </summary>
|
||||
public INetworkManager NetManager { get; internal set; }
|
||||
public INetworkManager NetManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
||||
@@ -230,24 +233,22 @@ namespace Emby.Server.Implementations
|
||||
/// </summary>
|
||||
protected ILogger<ApplicationHost> Logger { get; }
|
||||
|
||||
protected IServiceCollection ServiceCollection { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger factory.
|
||||
/// </summary>
|
||||
protected ILoggerFactory LoggerFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the application paths.
|
||||
/// Gets the application paths.
|
||||
/// </summary>
|
||||
/// <value>The application paths.</value>
|
||||
protected IServerApplicationPaths ApplicationPaths { get; set; }
|
||||
protected IServerApplicationPaths ApplicationPaths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration manager.
|
||||
/// Gets the configuration manager.
|
||||
/// </summary>
|
||||
/// <value>The configuration manager.</value>
|
||||
public ServerConfigurationManager ConfigurationManager { get; set; }
|
||||
public ServerConfigurationManager ConfigurationManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the service provider.
|
||||
@@ -264,11 +265,6 @@ namespace Emby.Server.Implementations
|
||||
/// </summary>
|
||||
public int HttpsPort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the PublishedServerUrl setting.
|
||||
/// </summary>
|
||||
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version ApplicationVersion { get; }
|
||||
|
||||
@@ -318,22 +314,6 @@ namespace Emby.Server.Implementations
|
||||
? Environment.MachineName
|
||||
: ConfigurationManager.Configuration.ServerName;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary function to migration network settings out of system.xml and into network.xml.
|
||||
/// TODO: remove at the point when a fixed migration path has been decided upon.
|
||||
/// </summary>
|
||||
private void MigrateNetworkConfiguration()
|
||||
{
|
||||
string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
var networkSettings = new NetworkConfiguration();
|
||||
ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
|
||||
_xmlSerializer.SerializeToFile(networkSettings, path);
|
||||
Logger.LogDebug("Successfully migrated network settings.");
|
||||
}
|
||||
}
|
||||
|
||||
public string ExpandVirtualPath(string path)
|
||||
{
|
||||
var appPaths = ApplicationPaths;
|
||||
@@ -350,22 +330,6 @@ namespace Emby.Server.Implementations
|
||||
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of type and resolves all constructor dependencies.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object CreateInstance(Type type)
|
||||
=> ActivatorUtilities.CreateInstance(ServiceProvider, type);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of type and resolves all constructor dependencies.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type.</typeparam>
|
||||
/// <returns>T.</returns>
|
||||
public T CreateInstance<T>()
|
||||
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the instance safe.
|
||||
/// </summary>
|
||||
@@ -375,7 +339,7 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
_creatingInstances ??= new List<Type>();
|
||||
|
||||
if (_creatingInstances.IndexOf(type) != -1)
|
||||
if (_creatingInstances.Contains(type))
|
||||
{
|
||||
Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
|
||||
foreach (var entry in _creatingInstances)
|
||||
@@ -385,7 +349,7 @@ namespace Emby.Server.Implementations
|
||||
|
||||
_pluginManager.FailPlugin(type.Assembly);
|
||||
|
||||
throw new ExternalException("DI Loop detected.");
|
||||
throw new TypeLoadException("DI Loop detected");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -418,8 +382,15 @@ namespace Emby.Server.Implementations
|
||||
public IEnumerable<Type> GetExportTypes<T>()
|
||||
{
|
||||
var currentType = typeof(T);
|
||||
|
||||
return _allConcreteTypes.Where(i => currentType.IsAssignableFrom(i));
|
||||
var numberOfConcreteTypes = _allConcreteTypes.Length;
|
||||
for (var i = 0; i < numberOfConcreteTypes; i++)
|
||||
{
|
||||
var type = _allConcreteTypes[i];
|
||||
if (currentType.IsAssignableFrom(type))
|
||||
{
|
||||
yield return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -434,9 +405,9 @@ namespace Emby.Server.Implementations
|
||||
|
||||
if (manageLifetime)
|
||||
{
|
||||
lock (_disposableParts)
|
||||
foreach (var part in parts.OfType<IDisposable>())
|
||||
{
|
||||
_disposableParts.AddRange(parts.OfType<IDisposable>());
|
||||
_disposableParts.TryAdd(part, byte.MinValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,9 +426,9 @@ namespace Emby.Server.Implementations
|
||||
|
||||
if (manageLifetime)
|
||||
{
|
||||
lock (_disposableParts)
|
||||
foreach (var part in parts.OfType<IDisposable>())
|
||||
{
|
||||
_disposableParts.AddRange(parts.OfType<IDisposable>());
|
||||
_disposableParts.TryAdd(part, byte.MinValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,14 +492,12 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Init()
|
||||
public void Init(IServiceCollection serviceCollection)
|
||||
{
|
||||
DiscoverTypes();
|
||||
|
||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||
|
||||
// Have to migrate settings here as migration subsystem not yet initialised.
|
||||
MigrateNetworkConfiguration();
|
||||
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
||||
|
||||
// Initialize runtime stat collection
|
||||
@@ -551,128 +520,130 @@ namespace Emby.Server.Implementations
|
||||
CertificatePath = networkConfiguration.CertificatePath;
|
||||
Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
|
||||
|
||||
RegisterServices();
|
||||
RegisterServices(serviceCollection);
|
||||
|
||||
_pluginManager.RegisterServices(ServiceCollection);
|
||||
_pluginManager.RegisterServices(serviceCollection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers services/resources with the service collection that will be available via DI.
|
||||
/// </summary>
|
||||
protected virtual void RegisterServices()
|
||||
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||
protected virtual void RegisterServices(IServiceCollection serviceCollection)
|
||||
{
|
||||
ServiceCollection.AddSingleton(_startupOptions);
|
||||
serviceCollection.AddSingleton(_startupOptions);
|
||||
|
||||
ServiceCollection.AddMemoryCache();
|
||||
serviceCollection.AddMemoryCache();
|
||||
|
||||
ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
|
||||
ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
|
||||
ServiceCollection.AddSingleton<IApplicationHost>(this);
|
||||
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
|
||||
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||
serviceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
|
||||
serviceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
|
||||
serviceCollection.AddSingleton<IApplicationHost>(this);
|
||||
serviceCollection.AddSingleton(_pluginManager);
|
||||
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||
|
||||
ServiceCollection.AddSingleton(_fileSystemManager);
|
||||
ServiceCollection.AddSingleton<TmdbClientManager>();
|
||||
serviceCollection.AddSingleton(_fileSystemManager);
|
||||
serviceCollection.AddSingleton<TmdbClientManager>();
|
||||
|
||||
ServiceCollection.AddSingleton(NetManager);
|
||||
serviceCollection.AddSingleton(NetManager);
|
||||
|
||||
ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
|
||||
serviceCollection.AddSingleton<ITaskManager, TaskManager>();
|
||||
|
||||
ServiceCollection.AddSingleton(_xmlSerializer);
|
||||
serviceCollection.AddSingleton(_xmlSerializer);
|
||||
|
||||
ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
|
||||
serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
|
||||
|
||||
ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
|
||||
serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
|
||||
|
||||
ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
|
||||
serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
|
||||
|
||||
ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
|
||||
serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IZipClient, ZipClient>();
|
||||
serviceCollection.AddSingleton<IZipClient, ZipClient>();
|
||||
|
||||
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
|
||||
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
||||
serviceCollection.AddSingleton<IServerApplicationHost>(this);
|
||||
serviceCollection.AddSingleton(ApplicationPaths);
|
||||
|
||||
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||
|
||||
ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||
ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||
|
||||
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||
ServiceCollection.AddSingleton<EncodingHelper>();
|
||||
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||
serviceCollection.AddSingleton<EncodingHelper>();
|
||||
|
||||
// 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<IProviderManager>(provider.GetRequiredService<IProviderManager>));
|
||||
ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
|
||||
ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
|
||||
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
||||
serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
|
||||
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
|
||||
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
|
||||
serviceCollection.AddSingleton<NamingOptions>();
|
||||
|
||||
ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
|
||||
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
|
||||
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
|
||||
|
||||
ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
|
||||
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
|
||||
|
||||
ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
|
||||
serviceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
|
||||
serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
|
||||
|
||||
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
|
||||
serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
|
||||
serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
|
||||
serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
|
||||
serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
|
||||
|
||||
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
|
||||
ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
|
||||
ServiceCollection.AddSingleton<IDtoService, DtoService>();
|
||||
serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
|
||||
serviceCollection.AddSingleton<IDtoService, DtoService>();
|
||||
|
||||
ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
|
||||
serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
|
||||
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
|
||||
serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
||||
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
||||
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
|
||||
serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<LiveTvDtoService>();
|
||||
ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
|
||||
serviceCollection.AddSingleton<LiveTvDtoService>();
|
||||
serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
|
||||
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
|
||||
serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
|
||||
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
|
||||
|
||||
ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
||||
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||
|
||||
ServiceCollection.AddScoped<ISessionContext, SessionContext>();
|
||||
serviceCollection.AddScoped<ISessionContext, SessionContext>();
|
||||
|
||||
ServiceCollection.AddSingleton<IAuthService, AuthService>();
|
||||
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
||||
serviceCollection.AddSingleton<IAuthService, AuthService>();
|
||||
serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
|
||||
serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
|
||||
|
||||
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
|
||||
ServiceCollection.AddSingleton<TranscodingJobHelper>();
|
||||
ServiceCollection.AddScoped<MediaInfoHelper>();
|
||||
ServiceCollection.AddScoped<AudioHelper>();
|
||||
ServiceCollection.AddScoped<DynamicHlsHelper>();
|
||||
|
||||
ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
|
||||
serviceCollection.AddSingleton<TranscodingJobHelper>();
|
||||
serviceCollection.AddScoped<MediaInfoHelper>();
|
||||
serviceCollection.AddScoped<AudioHelper>();
|
||||
serviceCollection.AddScoped<DynamicHlsHelper>();
|
||||
serviceCollection.AddScoped<IClientEventLogger, ClientEventLogger>();
|
||||
serviceCollection.AddSingleton<IDirectoryService, DirectoryService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -795,8 +766,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
_pluginManager.CreatePlugins();
|
||||
|
||||
_urlPrefixes = GetUrlPrefixes().ToArray();
|
||||
|
||||
Resolve<ILibraryManager>().AddParts(
|
||||
GetExports<IResolverIgnoreRule>(),
|
||||
GetExports<IItemResolver>(),
|
||||
@@ -864,32 +833,12 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetUrlPrefixes()
|
||||
{
|
||||
var hosts = new[] { "+" };
|
||||
|
||||
return hosts.SelectMany(i =>
|
||||
{
|
||||
var prefixes = new List<string>
|
||||
{
|
||||
"http://" + i + ":" + HttpPort + "/"
|
||||
};
|
||||
|
||||
if (Certificate != null)
|
||||
{
|
||||
prefixes.Add("https://" + i + ":" + HttpsPort + "/");
|
||||
}
|
||||
|
||||
return prefixes;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when [configuration updated].
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
|
||||
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
||||
private void OnConfigurationUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var requiresRestart = false;
|
||||
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
||||
@@ -898,8 +847,8 @@ namespace Emby.Server.Implementations
|
||||
if (HttpPort != 0 && HttpsPort != 0)
|
||||
{
|
||||
// Need to restart if ports have changed
|
||||
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
||||
networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||
if (networkConfiguration.HttpServerPortNumber != HttpPort
|
||||
|| networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||
{
|
||||
if (ConfigurationManager.Configuration.IsPortAuthorized)
|
||||
{
|
||||
@@ -911,11 +860,6 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
requiresRestart = true;
|
||||
}
|
||||
|
||||
if (ValidateSslCertificate(networkConfiguration))
|
||||
{
|
||||
requiresRestart = true;
|
||||
@@ -957,7 +901,7 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies that the kernel that a change has been made that requires a restart.
|
||||
/// Notifies the kernel that a change has been made that requires a restart.
|
||||
/// </summary>
|
||||
public void NotifyPendingRestart()
|
||||
{
|
||||
@@ -1030,7 +974,7 @@ namespace Emby.Server.Implementations
|
||||
yield return typeof(IServerApplicationHost).Assembly;
|
||||
|
||||
// Include composable parts in the Providers assembly
|
||||
yield return typeof(ProviderUtils).Assembly;
|
||||
yield return typeof(ProviderManager).Assembly;
|
||||
|
||||
// Include composable parts in the Photos assembly
|
||||
yield return typeof(PhotoProvider).Assembly;
|
||||
@@ -1056,6 +1000,9 @@ namespace Emby.Server.Implementations
|
||||
// Network
|
||||
yield return typeof(NetworkManager).Assembly;
|
||||
|
||||
// Hls
|
||||
yield return typeof(DynamicHlsPlaylistGenerator).Assembly;
|
||||
|
||||
foreach (var i in GetAssembliesWithPartsInternal())
|
||||
{
|
||||
yield return i;
|
||||
@@ -1098,11 +1045,6 @@ namespace Emby.Server.Implementations
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo()
|
||||
=> NetManager.GetMacAddresses()
|
||||
.Select(i => new WakeOnLanInfo(i))
|
||||
.ToList();
|
||||
|
||||
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
|
||||
{
|
||||
return new PublicSystemInfo
|
||||
@@ -1118,7 +1060,7 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
|
||||
public string GetSmartApiUrl(IPAddress remoteAddr)
|
||||
{
|
||||
// Published server ends with a /
|
||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||
@@ -1127,18 +1069,12 @@ namespace Emby.Server.Implementations
|
||||
return PublishedServerUrl.Trim('/');
|
||||
}
|
||||
|
||||
string smart = NetManager.GetBindInterface(remoteAddr, out port);
|
||||
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
||||
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return smart.Trim('/');
|
||||
}
|
||||
|
||||
string smart = NetManager.GetBindInterface(remoteAddr, out var port);
|
||||
return GetLocalApiUrl(smart.Trim('/'), null, port);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
||||
public string GetSmartApiUrl(HttpRequest request)
|
||||
{
|
||||
// Return the host in the HTTP request as the API url
|
||||
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
|
||||
@@ -1152,25 +1088,11 @@ namespace Emby.Server.Implementations
|
||||
return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
|
||||
}
|
||||
|
||||
// Published server ends with a /
|
||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||
{
|
||||
// Published server ends with a '/', so we need to remove it.
|
||||
return PublishedServerUrl.Trim('/');
|
||||
}
|
||||
|
||||
string smart = NetManager.GetBindInterface(request, out port);
|
||||
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
||||
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return smart.Trim('/');
|
||||
}
|
||||
|
||||
return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
|
||||
return GetSmartApiUrl(request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetSmartApiUrl(string hostname, int? port = null)
|
||||
public string GetSmartApiUrl(string hostname)
|
||||
{
|
||||
// Published server ends with a /
|
||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||
@@ -1179,38 +1101,38 @@ namespace Emby.Server.Implementations
|
||||
return PublishedServerUrl.Trim('/');
|
||||
}
|
||||
|
||||
string smart = NetManager.GetBindInterface(hostname, out port);
|
||||
|
||||
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
||||
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return smart.Trim('/');
|
||||
}
|
||||
|
||||
string smart = NetManager.GetBindInterface(hostname, out var port);
|
||||
return GetLocalApiUrl(smart.Trim('/'), null, port);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetLoopbackHttpApiUrl()
|
||||
public string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true)
|
||||
{
|
||||
if (NetManager.IsIP6Enabled)
|
||||
{
|
||||
return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort);
|
||||
}
|
||||
|
||||
return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
|
||||
// With an empty source, the port will be null
|
||||
var smart = NetManager.GetBindInterface(hostname ?? IPHost.None, out _);
|
||||
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
||||
int? port = !allowHttps ? HttpPort : null;
|
||||
return GetLocalApiUrl(smart, scheme, port);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
|
||||
{
|
||||
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
||||
if (hostname.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return hostname.TrimEnd('/');
|
||||
}
|
||||
|
||||
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
||||
// not. For consistency, always trim the trailing slash.
|
||||
scheme ??= ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
|
||||
var isHttps = string.Equals(scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
|
||||
return new UriBuilder
|
||||
{
|
||||
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
||||
Scheme = scheme,
|
||||
Host = hostname,
|
||||
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
||||
Port = port ?? (isHttps ? HttpsPort : HttpPort),
|
||||
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||
}.ToString().TrimEnd('/');
|
||||
}
|
||||
@@ -1277,12 +1199,15 @@ namespace Emby.Server.Implementations
|
||||
|
||||
Logger.LogInformation("Disposing {Type}", type.Name);
|
||||
|
||||
var parts = _disposableParts.Distinct().Where(i => i.GetType() != type).ToList();
|
||||
_disposableParts.Clear();
|
||||
|
||||
foreach (var part in parts)
|
||||
foreach (var (part, _) in _disposableParts)
|
||||
{
|
||||
Logger.LogInformation("Disposing {Type}", part.GetType().Name);
|
||||
var partType = part.GetType();
|
||||
if (partType == type)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Disposing {Type}", partType.Name);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1290,12 +1215,58 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name);
|
||||
Logger.LogError(ex, "Error disposing {Type}", partType.Name);
|
||||
}
|
||||
}
|
||||
|
||||
_disposableParts.Clear();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await DisposeAsyncCore().ConfigureAwait(false);
|
||||
Dispose(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
|
||||
/// </summary>
|
||||
/// <returns>A ValueTask.</returns>
|
||||
protected virtual async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
var type = GetType();
|
||||
|
||||
Logger.LogInformation("Disposing {Type}", type.Name);
|
||||
|
||||
foreach (var (part, _) in _disposableParts)
|
||||
{
|
||||
var partType = part.GetType();
|
||||
if (partType == type)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Disposing {Type}", partType.Name);
|
||||
|
||||
try
|
||||
{
|
||||
part.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error disposing {Type}", partType.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// used for closing websockets
|
||||
foreach (var session in _sessionManager.Sessions)
|
||||
{
|
||||
await session.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using System.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
using SharpCompress.Archives.SevenZip;
|
||||
using SharpCompress.Archives.Tar;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.GZip;
|
||||
using SharpCompress.Readers.Zip;
|
||||
|
||||
namespace Emby.Server.Implementations.Archiving
|
||||
{
|
||||
@@ -14,55 +11,6 @@ namespace Emby.Server.Implementations.Archiving
|
||||
/// </summary>
|
||||
public class ZipClient : IZipClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts all.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">The source file.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var fileStream = File.OpenRead(sourceFile);
|
||||
ExtractAll(fileStream, targetPath, overwriteExistingFiles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var reader = ReaderFactory.Open(source);
|
||||
var options = new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true
|
||||
};
|
||||
|
||||
if (overwriteExistingFiles)
|
||||
{
|
||||
options.Overwrite = true;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
reader.WriteAllToDirectory(targetPath, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var reader = ZipReader.Open(source);
|
||||
var options = new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = overwriteExistingFiles
|
||||
};
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
reader.WriteAllToDirectory(targetPath, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
@@ -94,69 +42,5 @@ namespace Emby.Server.Implementations.Archiving
|
||||
reader.WriteEntryToFile(Path.Combine(targetPath, filename));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from7z.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">The source file.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var fileStream = File.OpenRead(sourceFile);
|
||||
ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from7z.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var archive = SevenZipArchive.Open(source);
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
var options = new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = overwriteExistingFiles
|
||||
};
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
reader.WriteAllToDirectory(targetPath, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from tar.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">The source file.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var fileStream = File.OpenRead(sourceFile);
|
||||
ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from tar.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
|
||||
{
|
||||
using var archive = TarArchive.Open(source);
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
var options = new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = overwriteExistingFiles
|
||||
};
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
reader.WriteAllToDirectory(targetPath, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
@@ -38,7 +39,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
/// <summary>
|
||||
/// The LiveTV channel manager.
|
||||
/// </summary>
|
||||
public class ChannelManager : IChannelManager
|
||||
public class ChannelManager : IChannelManager, IDisposable
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
@@ -51,6 +52,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
|
||||
@@ -129,16 +131,14 @@ namespace Emby.Server.Implementations.Channels
|
||||
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
|
||||
if (internalChannel == null)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
throw new ArgumentException(nameof(item.ChannelId));
|
||||
}
|
||||
|
||||
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
|
||||
|
||||
var supportsDelete = channel as ISupportsDelete;
|
||||
|
||||
if (supportsDelete == null)
|
||||
if (channel is not ISupportsDelete supportsDelete)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
throw new ArgumentException(nameof(channel));
|
||||
}
|
||||
|
||||
return supportsDelete.DeleteItem(item.ExternalId, CancellationToken.None);
|
||||
@@ -162,7 +162,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
/// <inheritdoc />
|
||||
public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
|
||||
{
|
||||
var user = query.UserId.Equals(Guid.Empty)
|
||||
var user = query.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(query.UserId);
|
||||
|
||||
@@ -179,7 +179,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
try
|
||||
{
|
||||
return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
|
||||
&& hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
|
||||
&& hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == val;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -265,17 +265,16 @@ namespace Emby.Server.Implementations.Channels
|
||||
}
|
||||
}
|
||||
|
||||
return new QueryResult<Channel>
|
||||
{
|
||||
Items = all,
|
||||
TotalRecordCount = totalCount
|
||||
};
|
||||
return new QueryResult<Channel>(
|
||||
query.StartIndex,
|
||||
totalCount,
|
||||
all);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
|
||||
{
|
||||
var user = query.UserId.Equals(Guid.Empty)
|
||||
var user = query.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(query.UserId);
|
||||
|
||||
@@ -286,11 +285,10 @@ namespace Emby.Server.Implementations.Channels
|
||||
// TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues.
|
||||
var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user);
|
||||
|
||||
var result = new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = returnItems,
|
||||
TotalRecordCount = internalResult.TotalRecordCount
|
||||
};
|
||||
var result = new QueryResult<BaseItemDto>(
|
||||
query.StartIndex,
|
||||
internalResult.TotalRecordCount,
|
||||
returnItems);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -334,7 +332,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
private Channel GetChannelEntity(IChannel channel)
|
||||
{
|
||||
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result;
|
||||
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
|
||||
@@ -476,7 +474,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
item.ChannelId = id;
|
||||
|
||||
if (item.ParentId != parentFolderId)
|
||||
if (!item.ParentId.Equals(parentFolderId))
|
||||
{
|
||||
forceUpdate = true;
|
||||
}
|
||||
@@ -541,7 +539,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
return _libraryManager.GetItemIds(
|
||||
new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(Channel) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Channel },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
|
||||
}).Select(i => GetChannelFeatures(i)).ToArray();
|
||||
}
|
||||
@@ -621,11 +619,10 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
var returnItems = _dtoService.GetBaseItemDtos(items, query.DtoOptions, query.User);
|
||||
|
||||
var result = new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = returnItems,
|
||||
TotalRecordCount = totalRecordCount
|
||||
};
|
||||
var result = new QueryResult<BaseItemDto>(
|
||||
query.StartIndex,
|
||||
totalRecordCount,
|
||||
returnItems);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -718,7 +715,9 @@ namespace Emby.Server.Implementations.Channels
|
||||
// Find the corresponding channel provider plugin
|
||||
var channelProvider = GetChannelProvider(channel);
|
||||
|
||||
var parentItem = query.ParentId == Guid.Empty ? channel : _libraryManager.GetItemById(query.ParentId);
|
||||
var parentItem = query.ParentId.Equals(default)
|
||||
? channel
|
||||
: _libraryManager.GetItemById(query.ParentId);
|
||||
|
||||
var itemsResult = await GetChannelItems(
|
||||
channelProvider,
|
||||
@@ -729,7 +728,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (query.ParentId == Guid.Empty)
|
||||
if (query.ParentId.Equals(default))
|
||||
{
|
||||
query.Parent = channel;
|
||||
}
|
||||
@@ -787,11 +786,10 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, query.DtoOptions, query.User);
|
||||
|
||||
var result = new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = returnItems,
|
||||
TotalRecordCount = internalResult.TotalRecordCount
|
||||
};
|
||||
var result = new QueryResult<BaseItemDto>(
|
||||
query.StartIndex,
|
||||
internalResult.TotalRecordCount,
|
||||
returnItems);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1075,14 +1073,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
||||
// was used for status
|
||||
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
|
||||
// {
|
||||
// item.ExternalEtag = info.Etag;
|
||||
// forceUpdate = true;
|
||||
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
|
||||
// }
|
||||
|
||||
if (!internalChannelId.Equals(item.ChannelId))
|
||||
{
|
||||
forceUpdate = true;
|
||||
@@ -1143,7 +1133,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
if (!info.IsLiveStream)
|
||||
{
|
||||
if (item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
|
||||
if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
||||
@@ -1152,7 +1142,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
|
||||
if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
|
||||
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
||||
@@ -1226,5 +1216,31 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and optionally managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_resourcePool?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(Channel) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Channel },
|
||||
ExcludeItemIds = installedChannelIds.ToArray()
|
||||
});
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
public string Key => "RefreshInternetChannels";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var manager = (ChannelManager)_channelManager;
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
|
||||
if (parentFolder == null)
|
||||
{
|
||||
throw new ArgumentException();
|
||||
throw new ArgumentException(nameof(parentFolder));
|
||||
}
|
||||
|
||||
var path = Path.Combine(parentFolder.Path, folderName);
|
||||
@@ -265,7 +265,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
{
|
||||
var childItem = _libraryManager.GetItemById(guidId);
|
||||
|
||||
var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value == guidId) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase)));
|
||||
var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (child == null)
|
||||
{
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using static MediaBrowser.Common.Cryptography.Constants;
|
||||
using static MediaBrowser.Model.Cryptography.Constants;
|
||||
|
||||
namespace Emby.Server.Implementations.Cryptography
|
||||
{
|
||||
@@ -12,10 +14,7 @@ namespace Emby.Server.Implementations.Cryptography
|
||||
/// </summary>
|
||||
public class CryptographyProvider : ICryptoProvider
|
||||
{
|
||||
// FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
|
||||
// Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
|
||||
// there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
|
||||
// Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
|
||||
// TODO: remove when not needed for backwards compat
|
||||
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
|
||||
{
|
||||
"MD5",
|
||||
@@ -35,60 +34,81 @@ namespace Emby.Server.Implementations.Cryptography
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DefaultHashMethod => "PBKDF2";
|
||||
public string DefaultHashMethod => "PBKDF2-SHA512";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetSupportedHashMethods()
|
||||
=> _supportedHashMethods;
|
||||
|
||||
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
|
||||
public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
|
||||
{
|
||||
// downgrading for now as we need this library to be dotnetstandard compliant
|
||||
// with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
|
||||
if (method != DefaultHashMethod)
|
||||
{
|
||||
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
|
||||
}
|
||||
|
||||
using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
|
||||
return r.GetBytes(32);
|
||||
byte[] salt = GenerateSalt();
|
||||
return new PasswordHash(
|
||||
DefaultHashMethod,
|
||||
Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
DefaultIterations,
|
||||
HashAlgorithmName.SHA512,
|
||||
DefaultOutputLength),
|
||||
salt,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
|
||||
public bool Verify(PasswordHash hash, ReadOnlySpan<char> password)
|
||||
{
|
||||
if (hashMethod == DefaultHashMethod)
|
||||
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
|
||||
{
|
||||
return PBKDF2(hashMethod, bytes, salt, DefaultIterations);
|
||||
return hash.Hash.SequenceEqual(
|
||||
Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
hash.Salt,
|
||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||
HashAlgorithmName.SHA1,
|
||||
32));
|
||||
}
|
||||
|
||||
if (!_supportedHashMethods.Contains(hashMethod))
|
||||
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
|
||||
{
|
||||
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
|
||||
return hash.Hash.SequenceEqual(
|
||||
Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
hash.Salt,
|
||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||
HashAlgorithmName.SHA512,
|
||||
DefaultOutputLength));
|
||||
}
|
||||
|
||||
using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
|
||||
if (salt.Length == 0)
|
||||
if (!_supportedHashMethods.Contains(hash.Id))
|
||||
{
|
||||
return h.ComputeHash(bytes);
|
||||
throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
|
||||
}
|
||||
|
||||
byte[] salted = new byte[bytes.Length + salt.Length];
|
||||
using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}.");
|
||||
var bytes = Encoding.UTF8.GetBytes(password.ToArray());
|
||||
if (hash.Salt.Length == 0)
|
||||
{
|
||||
return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
|
||||
}
|
||||
|
||||
byte[] salted = new byte[bytes.Length + hash.Salt.Length];
|
||||
Array.Copy(bytes, salted, bytes.Length);
|
||||
Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
|
||||
return h.ComputeHash(salted);
|
||||
hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
|
||||
return hash.Hash.SequenceEqual(h.ComputeHash(salted));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
|
||||
=> PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] GenerateSalt()
|
||||
=> GenerateSalt(DefaultSaltLength);
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] GenerateSalt(int length)
|
||||
=> RandomNumberGenerator.GetBytes(length);
|
||||
{
|
||||
var salt = new byte[length];
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
rng.GetNonZeroBytes(salt);
|
||||
return salt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
@@ -160,21 +160,22 @@ namespace Emby.Server.Implementations.Data
|
||||
protected bool TableExists(ManagedConnection connection, string name)
|
||||
{
|
||||
return connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
|
||||
db =>
|
||||
{
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
|
||||
{
|
||||
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return true;
|
||||
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}, ReadTransactionMode);
|
||||
return false;
|
||||
},
|
||||
ReadTransactionMode);
|
||||
}
|
||||
|
||||
protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
|
||||
@@ -194,7 +195,7 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||
{
|
||||
if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase))
|
||||
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public class ManagedConnection : IDisposable
|
||||
public sealed class ManagedConnection : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _writeLock;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,6 @@ namespace Emby.Server.Implementations.Data
|
||||
DbFilePath = Path.Combine(appPaths.DataPath, "library.db");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "SQLite";
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the database.
|
||||
/// </summary>
|
||||
@@ -50,41 +47,42 @@ namespace Emby.Server.Implementations.Data
|
||||
var users = userDatasTableExists ? null : userManager.Users;
|
||||
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
db.ExecuteAll(string.Join(';', new[]
|
||||
db =>
|
||||
{
|
||||
"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)"
|
||||
}));
|
||||
|
||||
if (userDataTableExists)
|
||||
{
|
||||
var existingColumnNames = GetColumnNames(db, "userdata");
|
||||
|
||||
AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
||||
|
||||
if (!userDatasTableExists)
|
||||
db.ExecuteAll(string.Join(';', new[]
|
||||
{
|
||||
ImportUserIds(db, users);
|
||||
"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)",
|
||||
|
||||
db.ExecuteAll("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");
|
||||
"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)"
|
||||
}));
|
||||
|
||||
if (userDataTableExists)
|
||||
{
|
||||
var existingColumnNames = GetColumnNames(db, "userdata");
|
||||
|
||||
AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
||||
|
||||
if (!userDatasTableExists)
|
||||
{
|
||||
ImportUserIds(db, users);
|
||||
|
||||
db.ExecuteAll("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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, TransactionMode);
|
||||
},
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +99,7 @@ namespace Emby.Server.Implementations.Data
|
||||
continue;
|
||||
}
|
||||
|
||||
statement.TryBind("@UserId", user.Id.ToByteArray());
|
||||
statement.TryBind("@UserId", user.Id);
|
||||
statement.TryBind("@InternalUserId", user.InternalId);
|
||||
|
||||
statement.MoveNext();
|
||||
@@ -183,10 +181,11 @@ namespace Emby.Server.Implementations.Data
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
SaveUserData(db, internalUserId, key, userData);
|
||||
}, TransactionMode);
|
||||
db =>
|
||||
{
|
||||
SaveUserData(db, internalUserId, key, userData);
|
||||
},
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,13 +251,14 @@ namespace Emby.Server.Implementations.Data
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
foreach (var userItemData in userDataList)
|
||||
db =>
|
||||
{
|
||||
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
|
||||
}
|
||||
}, TransactionMode);
|
||||
foreach (var userItemData in userDataList)
|
||||
{
|
||||
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
|
||||
}
|
||||
},
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +387,7 @@ namespace Emby.Server.Implementations.Data
|
||||
return userData;
|
||||
}
|
||||
|
||||
#pragma warning disable CA2215
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and
|
||||
@@ -395,6 +396,10 @@ namespace Emby.Server.Implementations.Data
|
||||
/// </remarks>
|
||||
protected override void Dispose(bool dispose)
|
||||
{
|
||||
// The write lock and connection for the item repository are shared with the user data repository
|
||||
// since they point to the same database. The item repo has responsibility for disposing these two objects,
|
||||
// so the user data repo should not attempt to dispose them as well
|
||||
}
|
||||
#pragma warning restore CA2215
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Emby.Server.Implementations.Devices
|
||||
{
|
||||
var value = File.ReadAllText(CachePath, Encoding.UTF8);
|
||||
|
||||
if (Guid.TryParse(value, out var guid))
|
||||
if (Guid.TryParse(value, out _))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
@@ -21,7 +21,6 @@ using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@@ -109,7 +108,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
});
|
||||
|
||||
SetItemByNameInfo(item, dto, libraryItems, user);
|
||||
SetItemByNameInfo(item, dto, libraryItems);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,14 +133,11 @@ namespace Emby.Server.Implementations.Dto
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
||||
if (item is LiveTvChannel tvChannel)
|
||||
{
|
||||
var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) };
|
||||
LivetvManager.AddChannelInfo(list, options, user);
|
||||
LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
|
||||
}
|
||||
else if (item is LiveTvProgram)
|
||||
{
|
||||
var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) };
|
||||
var task = LivetvManager.AddInfoToProgramDto(list, options.Fields, user);
|
||||
Task.WaitAll(task);
|
||||
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (item is IItemByName itemByName
|
||||
@@ -156,8 +152,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
new DtoOptions(false)
|
||||
{
|
||||
EnableImages = false
|
||||
}),
|
||||
user);
|
||||
}));
|
||||
}
|
||||
|
||||
return dto;
|
||||
@@ -187,7 +182,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.People))
|
||||
{
|
||||
AttachPeople(dto, item);
|
||||
AttachPeople(dto, item, user);
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
|
||||
@@ -297,7 +292,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
path = path.TrimStart('.');
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
fileExtensionContainer = path;
|
||||
}
|
||||
@@ -314,13 +309,13 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
SetItemByNameInfo(item, dto, taggedItems, user);
|
||||
SetItemByNameInfo(item, dto, taggedItems);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems, User user = null)
|
||||
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems)
|
||||
{
|
||||
if (item is MusicArtist)
|
||||
{
|
||||
@@ -373,6 +368,12 @@ namespace Emby.Server.Implementations.Dto
|
||||
if (item is MusicAlbum || item is Season || item is Playlist)
|
||||
{
|
||||
dto.ChildCount = dto.RecursiveItemCount;
|
||||
var folderChildCount = folder.LinkedChildren.Length;
|
||||
// The default is an empty array, so we can't reliably use the count when it's empty
|
||||
if (folderChildCount > 0)
|
||||
{
|
||||
dto.ChildCount ??= folderChildCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.ChildCount))
|
||||
@@ -456,18 +457,13 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
}
|
||||
|
||||
private string GetDtoId(BaseItem item)
|
||||
{
|
||||
return item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.Album))
|
||||
{
|
||||
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new[] { nameof(MusicAlbum) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
|
||||
Name = item.Album,
|
||||
Limit = 1
|
||||
});
|
||||
@@ -497,7 +493,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting {imageType} image info for {path}", image.Type, image.Path);
|
||||
_logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -507,7 +503,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
/// </summary>
|
||||
/// <param name="dto">The dto.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
private void AttachPeople(BaseItemDto dto, BaseItem item)
|
||||
/// <param name="user">The requesting user.</param>
|
||||
private void AttachPeople(BaseItemDto dto, BaseItem item, User user = null)
|
||||
{
|
||||
// Ordering by person type to ensure actors and artists are at the front.
|
||||
// This is taking advantage of the fact that they both begin with A
|
||||
@@ -564,6 +561,9 @@ namespace Emby.Server.Implementations.Dto
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null)
|
||||
.Where(i => user == null ?
|
||||
true :
|
||||
i.IsVisible(user))
|
||||
.GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(x => x.First())
|
||||
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
|
||||
@@ -582,7 +582,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
if (dictionary.TryGetValue(person.Name, out Person entity))
|
||||
{
|
||||
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
|
||||
baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
baseItemPerson.Id = entity.Id;
|
||||
if (dto.ImageBlurHashes != null)
|
||||
{
|
||||
// Only add BlurHash for the person's image.
|
||||
@@ -741,8 +741,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.Tags = item.Tags;
|
||||
}
|
||||
|
||||
var hasAspectRatio = item as IHasAspectRatio;
|
||||
if (hasAspectRatio != null)
|
||||
if (item is IHasAspectRatio hasAspectRatio)
|
||||
{
|
||||
dto.AspectRatio = hasAspectRatio.AspectRatio;
|
||||
}
|
||||
@@ -755,15 +754,6 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.ScreenshotImageTags))
|
||||
{
|
||||
var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
|
||||
if (screenshotLimit > 0)
|
||||
{
|
||||
dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.Genres))
|
||||
{
|
||||
dto.Genres = item.Genres;
|
||||
@@ -901,15 +891,13 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.CommunityRating = item.CommunityRating;
|
||||
}
|
||||
|
||||
var supportsPlaceHolders = item as ISupportsPlaceHolders;
|
||||
if (supportsPlaceHolders != null && supportsPlaceHolders.IsPlaceHolder)
|
||||
if (item is ISupportsPlaceHolders supportsPlaceHolders && supportsPlaceHolders.IsPlaceHolder)
|
||||
{
|
||||
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
|
||||
}
|
||||
|
||||
// Add audio info
|
||||
var audio = item as Audio;
|
||||
if (audio != null)
|
||||
if (item is Audio audio)
|
||||
{
|
||||
dto.Album = audio.Album;
|
||||
if (audio.ExtraType.HasValue)
|
||||
@@ -982,8 +970,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
}).Where(i => i != null).ToArray();
|
||||
}
|
||||
|
||||
var hasAlbumArtist = item as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (item is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
|
||||
@@ -1109,12 +1096,13 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.LocalTrailerCount))
|
||||
{
|
||||
allExtras ??= item.GetExtras().ToArray();
|
||||
dto.LocalTrailerCount = allExtras.Count(i => i.ExtraType == ExtraType.Trailer);
|
||||
|
||||
if (item is IHasTrailers hasTrailers)
|
||||
{
|
||||
dto.LocalTrailerCount += hasTrailers.GetTrailerCount();
|
||||
dto.LocalTrailerCount = hasTrailers.LocalTrailers.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
dto.LocalTrailerCount = (allExtras ?? item.GetExtras()).Count(i => i.ExtraType == ExtraType.Trailer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1324,35 +1312,35 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
var allImages = parent.ImageInfos;
|
||||
|
||||
if (logoLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId == null)
|
||||
if (logoLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId is null)
|
||||
{
|
||||
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo);
|
||||
|
||||
if (image != null)
|
||||
{
|
||||
dto.ParentLogoItemId = GetDtoId(parent);
|
||||
dto.ParentLogoItemId = parent.Id;
|
||||
dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image);
|
||||
}
|
||||
}
|
||||
|
||||
if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId == null)
|
||||
if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId is null)
|
||||
{
|
||||
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art);
|
||||
|
||||
if (image != null)
|
||||
{
|
||||
dto.ParentArtItemId = GetDtoId(parent);
|
||||
dto.ParentArtItemId = parent.Id;
|
||||
dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image);
|
||||
}
|
||||
}
|
||||
|
||||
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && parent is not ICollectionFolder && parent is not UserView)
|
||||
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId is null || parent is Series) && parent is not ICollectionFolder && parent is not UserView)
|
||||
{
|
||||
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
|
||||
|
||||
if (image != null)
|
||||
{
|
||||
dto.ParentThumbItemId = GetDtoId(parent);
|
||||
dto.ParentThumbItemId = parent.Id;
|
||||
dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image);
|
||||
}
|
||||
}
|
||||
@@ -1363,7 +1351,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (images.Count > 0)
|
||||
{
|
||||
dto.ParentBackdropItemId = GetDtoId(parent);
|
||||
dto.ParentBackdropItemId = parent.Id;
|
||||
dto.ParentBackdropImageTags = GetTagsAndFillBlurhashes(dto, parent, ImageType.Backdrop, images);
|
||||
}
|
||||
}
|
||||
@@ -1410,44 +1398,27 @@ namespace Emby.Server.Implementations.Dto
|
||||
return null;
|
||||
}
|
||||
|
||||
ImageDimensions size;
|
||||
|
||||
var defaultAspectRatio = item.GetDefaultPrimaryImageAspectRatio();
|
||||
|
||||
if (defaultAspectRatio > 0)
|
||||
{
|
||||
return defaultAspectRatio;
|
||||
}
|
||||
|
||||
if (!imageInfo.IsLocalFile)
|
||||
{
|
||||
return null;
|
||||
return item.GetDefaultPrimaryImageAspectRatio();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
size = _imageProcessor.GetImageDimensions(item, imageInfo);
|
||||
|
||||
if (size.Width <= 0 || size.Height <= 0)
|
||||
var size = _imageProcessor.GetImageDimensions(item, imageInfo);
|
||||
var width = size.Width;
|
||||
var height = size.Height;
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
return null;
|
||||
return (double)width / height;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {0}", imageInfo.Path);
|
||||
return null;
|
||||
_logger.LogError(ex, "Failed to determine primary image aspect ratio for {ImagePath}", imageInfo.Path);
|
||||
}
|
||||
|
||||
var width = size.Width;
|
||||
var height = size.Height;
|
||||
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (double)width / height;
|
||||
return item.GetDefaultPrimaryImageAspectRatio();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,15 +24,15 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0-rc.2*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0-rc.2*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0-rc.2*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0-rc.2*" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.1" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
|
||||
<PackageReference Include="sharpcompress" Version="0.30.0" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.3" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
|
||||
<PackageReference Include="sharpcompress" Version="0.32.2" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
@@ -49,10 +49,18 @@
|
||||
<NoWarn>AD0001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ using Jellyfin.Networking.Configuration;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Mono.Nat;
|
||||
|
||||
@@ -27,7 +26,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly ILogger<ExternalPortForwarding> _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
|
||||
private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
|
||||
|
||||
@@ -42,17 +40,14 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="appHost">The application host.</param>
|
||||
/// <param name="config">The configuration manager.</param>
|
||||
/// <param name="deviceDiscovery">The device discovery.</param>
|
||||
public ExternalPortForwarding(
|
||||
ILogger<ExternalPortForwarding> logger,
|
||||
IServerApplicationHost appHost,
|
||||
IServerConfigurationManager config,
|
||||
IDeviceDiscovery deviceDiscovery)
|
||||
IServerConfigurationManager config)
|
||||
{
|
||||
_logger = logger;
|
||||
_appHost = appHost;
|
||||
_config = config;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
}
|
||||
|
||||
private string GetConfigIdentifier()
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
}
|
||||
}
|
||||
|
||||
_lastProgressMessageTimes.AddOrUpdate(item.Id, key => DateTime.UtcNow, (key, existing) => DateTime.UtcNow);
|
||||
_lastProgressMessageTimes.AddOrUpdate(item.Id, _ => DateTime.UtcNow, (_, _) => DateTime.UtcNow);
|
||||
|
||||
var dict = new Dictionary<string, string>();
|
||||
dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
@@ -144,7 +144,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
|
||||
|
||||
_lastProgressMessageTimes.TryRemove(e.Argument.Id, out DateTime removed);
|
||||
_lastProgressMessageTimes.TryRemove(e.Argument.Id, out _);
|
||||
}
|
||||
|
||||
private static bool EnableRefreshMessage(BaseItem item)
|
||||
@@ -326,7 +326,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
var userIds = _sessionManager.Sessions
|
||||
.Select(i => i.UserId)
|
||||
.Where(i => !i.Equals(Guid.Empty))
|
||||
.Where(i => !i.Equals(default))
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
@@ -423,7 +423,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
continue;
|
||||
}
|
||||
|
||||
var collectionFolders = _libraryManager.GetCollectionFolders(item, allUserRootChildren);
|
||||
foreach (var folder in allUserRootChildren)
|
||||
{
|
||||
list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture));
|
||||
@@ -465,6 +464,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Udp;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -26,6 +28,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
private readonly ILogger<UdpServerEntryPoint> _logger;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// The UDP server.
|
||||
@@ -40,14 +43,17 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
|
||||
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
|
||||
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
public UdpServerEntryPoint(
|
||||
ILogger<UdpServerEntryPoint> logger,
|
||||
IServerApplicationHost appHost,
|
||||
IConfiguration configuration)
|
||||
IConfiguration configuration,
|
||||
IConfigurationManager configurationManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_appHost = appHost;
|
||||
_config = configuration;
|
||||
_configurationManager = configurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -55,6 +61,11 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
var changes = _changedItems.ToList();
|
||||
_changedItems.Clear();
|
||||
|
||||
var task = SendNotifications(changes, CancellationToken.None);
|
||||
SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
if (_updateTimer != null)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
@@ -24,7 +23,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
|
||||
if (!auth.HasToken)
|
||||
{
|
||||
throw new AuthenticationException("Request does not contain a token.");
|
||||
return auth;
|
||||
}
|
||||
|
||||
if (!auth.IsAuthenticated)
|
||||
|
||||
@@ -47,7 +47,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
var session = await GetSession(requestContext).ConfigureAwait(false);
|
||||
|
||||
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
|
||||
return session.UserId.Equals(default)
|
||||
? null
|
||||
: _userManager.GetUserById(session.UserId);
|
||||
}
|
||||
|
||||
public Task<User?> GetUser(object requestContext)
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <summary>
|
||||
/// Class WebSocketConnection.
|
||||
/// </summary>
|
||||
public class WebSocketConnection : IWebSocketConnection, IDisposable
|
||||
public class WebSocketConnection : IWebSocketConnection
|
||||
{
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
@@ -36,23 +36,22 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// </summary>
|
||||
private readonly WebSocket _socket;
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="socket">The socket.</param>
|
||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||
/// <param name="query">The query.</param>
|
||||
public WebSocketConnection(
|
||||
ILogger<WebSocketConnection> logger,
|
||||
WebSocket socket,
|
||||
IPAddress? remoteEndPoint,
|
||||
IQueryCollection query)
|
||||
IPAddress? remoteEndPoint)
|
||||
{
|
||||
_logger = logger;
|
||||
_socket = socket;
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
QueryString = query;
|
||||
|
||||
_jsonOptions = JsonDefaults.Options;
|
||||
LastActivityDate = DateTime.Now;
|
||||
@@ -81,12 +80,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <inheritdoc />
|
||||
public DateTime LastKeepAliveDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the query string.
|
||||
/// </summary>
|
||||
/// <value>The query string.</value>
|
||||
public IQueryCollection QueryString { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state.
|
||||
/// </summary>
|
||||
@@ -180,7 +173,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
|
||||
WebSocketMessage<object>? stub;
|
||||
long bytesConsumed = 0;
|
||||
long bytesConsumed;
|
||||
try
|
||||
{
|
||||
stub = DeserializeWebSocketMessage(buffer, out bytesConsumed);
|
||||
@@ -236,7 +229,8 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
MessageId = Guid.NewGuid(),
|
||||
MessageType = SessionMessageType.KeepAlive
|
||||
}, CancellationToken.None);
|
||||
},
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -252,10 +246,39 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <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)
|
||||
{
|
||||
_socket.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await DisposeAsyncCore().ConfigureAwait(false);
|
||||
Dispose(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
|
||||
/// </summary>
|
||||
/// <returns>A ValueTask.</returns>
|
||||
protected virtual async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
if (_socket.State == WebSocketState.Open)
|
||||
{
|
||||
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "System Shutdown", CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_socket.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -35,7 +36,12 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <inheritdoc />
|
||||
public async Task WebSocketRequestHandler(HttpContext context)
|
||||
{
|
||||
_ = await _authService.Authenticate(context.Request).ConfigureAwait(false);
|
||||
var authorizationInfo = await _authService.Authenticate(context.Request).ConfigureAwait(false);
|
||||
if (!authorizationInfo.IsAuthenticated)
|
||||
{
|
||||
throw new SecurityException("Token is required");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
|
||||
@@ -45,8 +51,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
using var connection = new WebSocketConnection(
|
||||
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
||||
webSocket,
|
||||
context.Connection.RemoteIpAddress,
|
||||
context.Request.Query)
|
||||
context.GetNormalizedRemoteIp())
|
||||
{
|
||||
OnReceive = ProcessWebSocketMessageReceived
|
||||
};
|
||||
@@ -54,7 +59,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
var tasks = new Task[_webSocketListeners.Length];
|
||||
for (var i = 0; i < _webSocketListeners.Length; ++i)
|
||||
{
|
||||
tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection);
|
||||
tasks[i] = _webSocketListeners[i].ProcessWebSocketConnectedAsync(connection, context);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
public class FileRefresher : IDisposable
|
||||
public sealed class FileRefresher : IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
private readonly List<string> _affectedPaths = new List<string>();
|
||||
private readonly object _timerLock = new object();
|
||||
private Timer _timer;
|
||||
private Timer? _timer;
|
||||
private bool _disposed;
|
||||
|
||||
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
||||
@@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO
|
||||
AddPath(path);
|
||||
}
|
||||
|
||||
public event EventHandler<EventArgs> Completed;
|
||||
public event EventHandler<EventArgs>? Completed;
|
||||
|
||||
public string Path { get; private set; }
|
||||
|
||||
@@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO
|
||||
RestartTimer();
|
||||
}
|
||||
|
||||
private void OnTimerCallback(object state)
|
||||
private void OnTimerCallback(object? state)
|
||||
{
|
||||
List<string> paths;
|
||||
|
||||
@@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
try
|
||||
{
|
||||
ProcessPathChanges(paths.ToList());
|
||||
ProcessPathChanges(paths);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
private void ProcessPathChanges(List<string> paths)
|
||||
{
|
||||
var itemsToRefresh = paths
|
||||
IEnumerable<BaseItem> itemsToRefresh = paths
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(GetAffectedBaseItem)
|
||||
.Where(item => item != null)
|
||||
.GroupBy(x => x.Id)
|
||||
.Select(x => x.First());
|
||||
.GroupBy(x => x!.Id) // Removed null values in the previous .Where()
|
||||
.Select(x => x.First())!;
|
||||
|
||||
foreach (var item in itemsToRefresh)
|
||||
{
|
||||
@@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
private BaseItem GetAffectedBaseItem(string path)
|
||||
private BaseItem? GetAffectedBaseItem(string path)
|
||||
{
|
||||
BaseItem item = null;
|
||||
BaseItem? item = null;
|
||||
|
||||
while (item == null && !string.IsNullOrEmpty(path))
|
||||
{
|
||||
item = _libraryManager.FindByPath(path, null);
|
||||
|
||||
path = System.IO.Path.GetDirectoryName(path);
|
||||
path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
|
||||
}
|
||||
|
||||
if (item != null)
|
||||
@@ -219,8 +217,13 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DisposeTimer();
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace Emby.Server.Implementations.IO
|
||||
// But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
|
||||
await Task.Delay(45000).ConfigureAwait(false);
|
||||
|
||||
_tempIgnoredPaths.TryRemove(path, out var val);
|
||||
_tempIgnoredPaths.TryRemove(path, out _);
|
||||
|
||||
if (refreshPath)
|
||||
{
|
||||
@@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.IO
|
||||
if (_fileSystemWatchers.TryAdd(path, newWatcher))
|
||||
{
|
||||
newWatcher.EnableRaisingEvents = true;
|
||||
_logger.LogInformation("Watching directory " + path);
|
||||
_logger.LogInformation("Watching directory {Path}", path);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -276,7 +276,7 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error watching path: {path}", path);
|
||||
_logger.LogError(ex, "Error watching path: {Path}", path);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -449,12 +449,12 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
|
||||
var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
|
||||
newRefresher.Completed += NewRefresher_Completed;
|
||||
newRefresher.Completed += OnNewRefresherCompleted;
|
||||
_activeRefreshers.Add(newRefresher);
|
||||
}
|
||||
}
|
||||
|
||||
private void NewRefresher_Completed(object sender, EventArgs e)
|
||||
private void OnNewRefresherCompleted(object sender, EventArgs e)
|
||||
{
|
||||
var refresher = (FileRefresher)sender;
|
||||
DisposeRefresher(refresher);
|
||||
@@ -481,6 +481,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
lock (_activeRefreshers)
|
||||
{
|
||||
refresher.Completed -= OnNewRefresherCompleted;
|
||||
refresher.Dispose();
|
||||
_activeRefreshers.Remove(refresher);
|
||||
}
|
||||
@@ -492,6 +493,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
foreach (var refresher in _activeRefreshers.ToList())
|
||||
{
|
||||
refresher.Completed -= OnNewRefresherCompleted;
|
||||
refresher.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@@ -23,6 +21,11 @@ namespace Emby.Server.Implementations.IO
|
||||
private readonly string _tempPath;
|
||||
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
|
||||
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
|
||||
public ManagedFileSystem(
|
||||
ILogger<ManagedFileSystem> logger,
|
||||
IApplicationPaths applicationPaths)
|
||||
@@ -31,6 +34,7 @@ namespace Emby.Server.Implementations.IO
|
||||
_tempPath = applicationPaths.TempDirectory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void AddShortcutHandler(IShortcutHandler handler)
|
||||
{
|
||||
_shortcutHandlers.Add(handler);
|
||||
@@ -72,6 +76,7 @@ namespace Emby.Server.Implementations.IO
|
||||
return handler?.Resolve(filename);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string MakeAbsolutePath(string folderPath, string filePath)
|
||||
{
|
||||
// path is actually a stream
|
||||
@@ -257,6 +262,10 @@ namespace Emby.Server.Implementations.IO
|
||||
_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)
|
||||
{
|
||||
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,11 +367,13 @@ namespace Emby.Server.Implementations.IO
|
||||
return GetCreationTimeUtc(GetFileSystemInfo(path));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual DateTime GetCreationTimeUtc(FileSystemMetadata info)
|
||||
{
|
||||
return info.CreationTimeUtc;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual DateTime GetLastWriteTimeUtc(FileSystemMetadata info)
|
||||
{
|
||||
return info.LastWriteTimeUtc;
|
||||
@@ -397,6 +408,7 @@ namespace Emby.Server.Implementations.IO
|
||||
return GetLastWriteTimeUtc(GetFileSystemInfo(path));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void SetHidden(string path, bool isHidden)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
@@ -421,6 +433,7 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void SetAttributes(string path, bool isHidden, bool readOnly)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
@@ -444,7 +457,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
if (readOnly)
|
||||
{
|
||||
attributes = attributes | FileAttributes.ReadOnly;
|
||||
attributes |= FileAttributes.ReadOnly;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -453,7 +466,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
if (isHidden)
|
||||
{
|
||||
attributes = attributes | FileAttributes.Hidden;
|
||||
attributes |= FileAttributes.Hidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -498,6 +511,7 @@ namespace Emby.Server.Implementations.IO
|
||||
File.Copy(temp1, file2, true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool ContainsSubPath(string parentPath, string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(parentPath))
|
||||
@@ -515,6 +529,7 @@ namespace Emby.Server.Implementations.IO
|
||||
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string NormalizePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
@@ -530,24 +545,16 @@ namespace Emby.Server.Implementations.IO
|
||||
return Path.TrimEndingDirectorySeparator(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool AreEqual(string path1, string path2)
|
||||
{
|
||||
if (path1 == null && path2 == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (path1 == null || path2 == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return string.Equals(
|
||||
NormalizePath(path1),
|
||||
NormalizePath(path2),
|
||||
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string GetFileNameWithoutExtension(FileSystemMetadata info)
|
||||
{
|
||||
if (info.IsDirectory)
|
||||
@@ -558,11 +565,11 @@ namespace Emby.Server.Implementations.IO
|
||||
return Path.GetFileNameWithoutExtension(info.FullName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool IsPathFile(string path)
|
||||
{
|
||||
// Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\
|
||||
if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 &&
|
||||
!path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
|
||||
if (path.Contains("://", StringComparison.OrdinalIgnoreCase)
|
||||
&& !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -570,35 +577,44 @@ namespace Emby.Server.Implementations.IO
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void DeleteFile(string path)
|
||||
{
|
||||
SetAttributes(path, false, false);
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
public virtual List<FileSystemMetadata> GetDrives()
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<FileSystemMetadata> GetDrives()
|
||||
{
|
||||
// check for ready state to avoid waiting for drives to timeout
|
||||
// some drives on linux have no actual size or are used for other purposes
|
||||
return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram)
|
||||
return DriveInfo.GetDrives()
|
||||
.Where(
|
||||
d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
|
||||
&& d.IsReady
|
||||
&& d.TotalSize != 0)
|
||||
.Select(d => new FileSystemMetadata
|
||||
{
|
||||
Name = d.Name,
|
||||
FullName = d.RootDirectory.FullName,
|
||||
IsDirectory = true
|
||||
}).ToList();
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
|
||||
{
|
||||
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
|
||||
{
|
||||
return GetFiles(path, null, false, recursive);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||
{
|
||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||
@@ -629,6 +645,7 @@ namespace Emby.Server.Implementations.IO
|
||||
return ToMetadata(files);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
|
||||
{
|
||||
var directoryInfo = new DirectoryInfo(path);
|
||||
@@ -642,16 +659,19 @@ namespace Emby.Server.Implementations.IO
|
||||
return infos.Select(GetFileSystemMetadata);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
|
||||
{
|
||||
return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
|
||||
{
|
||||
return GetFilePaths(path, null, false, recursive);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||
{
|
||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||
@@ -682,11 +702,24 @@ namespace Emby.Server.Implementations.IO
|
||||
return files;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
|
||||
{
|
||||
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool DirectoryExists(string path)
|
||||
{
|
||||
return Directory.Exists(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool FileExists(string path)
|
||||
{
|
||||
return File.Exists(path);
|
||||
}
|
||||
|
||||
private EnumerationOptions GetEnumerationOptions(bool recursive)
|
||||
{
|
||||
return new EnumerationOptions
|
||||
|
||||
@@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO
|
||||
try
|
||||
{
|
||||
int read;
|
||||
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
while ((read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (onStarted != null)
|
||||
{
|
||||
@@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO
|
||||
if (emptyReadLimit <= 0)
|
||||
{
|
||||
int read;
|
||||
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
while ((read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
var bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
eofCount = 0;
|
||||
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,13 +88,13 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
var bytesToWrite = Math.Min(bytesRead, copyLength);
|
||||
|
||||
if (bytesToWrite > 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer.AsMemory(0, Convert.ToInt32(bytesToWrite)), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
copyLength -= bytesToWrite;
|
||||
@@ -137,9 +137,9 @@ namespace Emby.Server.Implementations.IO
|
||||
int bytesRead;
|
||||
int totalBytesRead = 0;
|
||||
|
||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.Images
|
||||
|
||||
protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items)
|
||||
{
|
||||
var useBackdrop = primaryItem is CollectionFolder;
|
||||
var useBackdrop = primaryItem is CollectionFolder || primaryItem is UserView;
|
||||
return items
|
||||
.Select(i =>
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.Images
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
|
||||
protected BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager)
|
||||
: base(fileSystem, providerManager, applicationPaths, imageProcessor)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
|
||||
@@ -28,35 +28,39 @@ namespace Emby.Server.Implementations.Images
|
||||
var view = (CollectionFolder)item;
|
||||
var viewType = view.CollectionType;
|
||||
|
||||
string[] includeItemTypes;
|
||||
BaseItemKind[] includeItemTypes;
|
||||
|
||||
if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Movie" };
|
||||
includeItemTypes = new[] { BaseItemKind.Movie };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Series" };
|
||||
includeItemTypes = new[] { BaseItemKind.Series };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "MusicAlbum" };
|
||||
includeItemTypes = new[] { BaseItemKind.MusicAlbum };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.MusicVideos, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new[] { BaseItemKind.MusicVideo };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Book", "AudioBook" };
|
||||
includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "BoxSet" };
|
||||
includeItemTypes = new[] { BaseItemKind.BoxSet };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Video", "Photo" };
|
||||
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
|
||||
}
|
||||
else
|
||||
{
|
||||
includeItemTypes = new string[] { "Video", "Audio", "Photo", "Movie", "Series" };
|
||||
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
|
||||
}
|
||||
|
||||
var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -68,9 +72,9 @@ namespace Emby.Server.Implementations.Images
|
||||
DtoOptions = new DtoOptions(false),
|
||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||
Limit = 8,
|
||||
OrderBy = new ValueTuple<string, SortOrder>[]
|
||||
OrderBy = new[]
|
||||
{
|
||||
new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending)
|
||||
(ItemSortBy.Random, SortOrder.Ascending)
|
||||
},
|
||||
IncludeItemTypes = includeItemTypes
|
||||
});
|
||||
|
||||
@@ -6,6 +6,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
@@ -34,14 +36,14 @@ namespace Emby.Server.Implementations.Images
|
||||
var view = (UserView)item;
|
||||
|
||||
var isUsingCollectionStrip = IsUsingCollectionStrip(view);
|
||||
var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
|
||||
var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var result = view.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
User = view.UserId.HasValue ? _userManager.GetUserById(view.UserId.Value) : null,
|
||||
CollapseBoxSetItems = false,
|
||||
Recursive = recursive,
|
||||
ExcludeItemTypes = new[] { "UserView", "CollectionFolder", "Person" },
|
||||
ExcludeItemTypes = new[] { BaseItemKind.UserView, BaseItemKind.CollectionFolder, BaseItemKind.Person },
|
||||
DtoOptions = new DtoOptions(false)
|
||||
});
|
||||
|
||||
@@ -82,16 +84,20 @@ namespace Emby.Server.Implementations.Images
|
||||
}).GroupBy(x => x.Id)
|
||||
.Select(x => x.First());
|
||||
|
||||
List<BaseItem> returnItems;
|
||||
if (isUsingCollectionStrip)
|
||||
{
|
||||
return items
|
||||
returnItems = items
|
||||
.Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb))
|
||||
.ToList();
|
||||
returnItems.Shuffle();
|
||||
return returnItems;
|
||||
}
|
||||
|
||||
return items
|
||||
returnItems = items
|
||||
.Where(i => i.HasImage(ImageType.Primary))
|
||||
.ToList();
|
||||
returnItems.Shuffle();
|
||||
return returnItems;
|
||||
}
|
||||
|
||||
protected override bool Supports(BaseItem item)
|
||||
|
||||
@@ -8,8 +8,6 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@@ -43,7 +41,7 @@ namespace Emby.Server.Implementations.Images
|
||||
return _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Genres = new[] { item.Name },
|
||||
IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
|
||||
IncludeItemTypes = new[] { BaseItemKind.Series, BaseItemKind.Movie },
|
||||
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
|
||||
Limit = 4,
|
||||
Recursive = true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user