mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-11 19:43:07 +03:00
Compare commits
496 Commits
openapi-ca
...
v10.8.13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e93d03d8cb | ||
|
|
a656799dc8 | ||
|
|
83d2c69516 | ||
|
|
204fdeb035 | ||
|
|
c4cdcb73fc | ||
|
|
1e0bd32358 | ||
|
|
bf5f00a383 | ||
|
|
0430ffecb6 | ||
|
|
85cfd080f1 | ||
|
|
a173d01139 | ||
|
|
3e7fad55de | ||
|
|
8cd685a4e9 | ||
|
|
9d565bbb83 | ||
|
|
ab855af95e | ||
|
|
0bac1eab98 | ||
|
|
97c2ba0115 | ||
|
|
4df1003029 | ||
|
|
3269ce56ca | ||
|
|
ce8eddd484 | ||
|
|
5e2872509a | ||
|
|
d3f4dcf6f6 | ||
|
|
be5e10ac37 | ||
|
|
b85a0288a7 | ||
|
|
f8fd851961 | ||
|
|
757f88b1a2 | ||
|
|
fa732bf4a1 | ||
|
|
4f6edd9c3c | ||
|
|
768497d0ff | ||
|
|
f1dc7d3a66 | ||
|
|
a732a28229 | ||
|
|
17626b8e48 | ||
|
|
98c6c34fbb | ||
|
|
bec8d7b3f5 | ||
|
|
2acae258b8 | ||
|
|
643df48707 | ||
|
|
2b98ce052e | ||
|
|
702347df50 | ||
|
|
9e5aa3e87e | ||
|
|
2cd29d1cfd | ||
|
|
eba95cc7f0 | ||
|
|
82ad2633fd | ||
|
|
d9f5619c9a | ||
|
|
d5a8419bc5 | ||
|
|
c448a4f6a5 | ||
|
|
faac37bcf9 | ||
|
|
5921379a29 | ||
|
|
79bb7560dc | ||
|
|
bf37db7f42 | ||
|
|
0ad70bb699 | ||
|
|
e6313d01eb | ||
|
|
876a6b9aec | ||
|
|
e0344353cd | ||
|
|
9799136daf | ||
|
|
3a5503be5f | ||
|
|
2cc0869144 | ||
|
|
3d735e242a | ||
|
|
31712e5da9 | ||
|
|
060097703b | ||
|
|
233e079e58 | ||
|
|
eafd785eb6 | ||
|
|
9908dad045 | ||
|
|
2b4bf81575 | ||
|
|
0c7ceb1545 | ||
|
|
173a963dbf | ||
|
|
6821a2ab35 | ||
|
|
efc79295de | ||
|
|
4d1a583297 | ||
|
|
c94a99fced | ||
|
|
5fdea32dca | ||
|
|
d1c668e230 | ||
|
|
6d662b6587 | ||
|
|
22a8283a9e | ||
|
|
0d9d2e0690 | ||
|
|
edaba7dbe5 | ||
|
|
e8b0ae07af | ||
|
|
c807712246 | ||
|
|
9a14a624a8 | ||
|
|
037eeed746 | ||
|
|
8ecb9558e2 | ||
|
|
8d04c98e35 | ||
|
|
09f1c7f535 | ||
|
|
0ac18a50f5 | ||
|
|
4ebae248df | ||
|
|
fbb9acf58b | ||
|
|
87f081c8ac | ||
|
|
44077b4f5c | ||
|
|
060a80bef7 | ||
|
|
885a1b02c1 | ||
|
|
1dea309ae4 | ||
|
|
32227c76b7 | ||
|
|
a7c43643a4 | ||
|
|
2a5efeb3bb | ||
|
|
464136cfc9 | ||
|
|
31673cc27d | ||
|
|
6a909f956e | ||
|
|
20e9db8308 | ||
|
|
f8b8fdace6 | ||
|
|
2a6e292153 | ||
|
|
c9f3d9bdde | ||
|
|
8d49e0099c | ||
|
|
76e3da6a40 | ||
|
|
f0faddcc44 | ||
|
|
e6606d41ce | ||
|
|
7c8aea7859 | ||
|
|
c4c5af40a1 | ||
|
|
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 |
@@ -27,6 +27,7 @@
|
||||
- [cvium](https://github.com/cvium)
|
||||
- [dannymichel](https://github.com/dannymichel)
|
||||
- [DaveChild](https://github.com/DaveChild)
|
||||
- [DavidFair](https://github.com/DavidFair)
|
||||
- [Delgan](https://github.com/Delgan)
|
||||
- [dcrdev](https://github.com/dcrdev)
|
||||
- [dhartung](https://github.com/dhartung)
|
||||
@@ -36,6 +37,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)
|
||||
@@ -157,6 +159,9 @@
|
||||
- [jonas-resch](https://github.com/jonas-resch)
|
||||
- [vgambier](https://github.com/vgambier)
|
||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
||||
- [TheTyrius](https://github.com/TheTyrius)
|
||||
- [Çağrı Sakaoğlu](https://github.com/ilovepilav)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
@@ -225,3 +230,6 @@
|
||||
- [gnuyent](https://github.com/gnuyent)
|
||||
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
||||
- [Jakob Kukla](https://github.com/jakobkukla)
|
||||
- [Utku Özdemir](https://github.com/utkuozdemir)
|
||||
- [JPUC1143](https://github.com/Jpuc1143/)
|
||||
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
||||
|
||||
@@ -72,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Configuration
|
||||
public DlnaOptions()
|
||||
{
|
||||
EnablePlayTo = true;
|
||||
EnableServer = true;
|
||||
EnableServer = false;
|
||||
BlastAliveMessages = true;
|
||||
SendOnlyMatchedHost = true;
|
||||
ClientDiscoveryIntervalSeconds = 60;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -313,7 +313,7 @@ namespace Emby.Dlna.Main
|
||||
|
||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
|
||||
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(false) + descriptorUri);
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri);
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
{
|
||||
|
||||
@@ -561,6 +561,7 @@ namespace Emby.Dlna.PlayTo
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -395,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;
|
||||
}
|
||||
@@ -403,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);
|
||||
@@ -439,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>
|
||||
|
||||
@@ -48,7 +48,6 @@ namespace Emby.Naming.Common
|
||||
".mkv",
|
||||
".mk3d",
|
||||
".mov",
|
||||
".mp2",
|
||||
".mp4",
|
||||
".mpe",
|
||||
".mpeg",
|
||||
@@ -251,7 +250,6 @@ namespace Emby.Naming.Common
|
||||
".sfx",
|
||||
".shn",
|
||||
".sid",
|
||||
".spc",
|
||||
".stm",
|
||||
".strm",
|
||||
".ult",
|
||||
@@ -315,7 +313,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
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.8.0</VersionPrefix>
|
||||
<VersionPrefix>10.8.13</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="TagLibSharp" Version="2.2.0" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -398,6 +398,12 @@ namespace Emby.Server.Implementations.AppBase
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConfigurationStore[] GetConfigurationStores()
|
||||
{
|
||||
return _configurationStores;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type GetConfigurationType(string key)
|
||||
{
|
||||
|
||||
@@ -111,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.
|
||||
@@ -1088,15 +1088,7 @@ 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 var port);
|
||||
return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
|
||||
return GetSmartApiUrl(request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -1114,13 +1106,13 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetApiUrlForLocalAccess(bool allowHttps = true)
|
||||
public string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true)
|
||||
{
|
||||
// With an empty source, the port will be null
|
||||
string smart = NetManager.GetBindInterface(string.Empty, out _);
|
||||
var smart = NetManager.GetBindInterface(hostname ?? IPHost.None, out _);
|
||||
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
||||
int? port = !allowHttps ? HttpPort : null;
|
||||
return GetLocalApiUrl(smart.Trim('/'), scheme, port);
|
||||
return GetLocalApiUrl(smart, scheme, port);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -1134,11 +1126,13 @@ namespace Emby.Server.Implementations
|
||||
|
||||
// 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('/');
|
||||
}
|
||||
@@ -1230,5 +1224,49 @@ namespace Emby.Server.Implementations
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Diacritics.Extensions;
|
||||
using Emby.Server.Implementations.Playlists;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
@@ -171,7 +170,15 @@ namespace Emby.Server.Implementations.Data
|
||||
"CodecTimeBase",
|
||||
"ColorPrimaries",
|
||||
"ColorSpace",
|
||||
"ColorTransfer"
|
||||
"ColorTransfer",
|
||||
"DvVersionMajor",
|
||||
"DvVersionMinor",
|
||||
"DvProfile",
|
||||
"DvLevel",
|
||||
"RpuPresentFlag",
|
||||
"ElPresentFlag",
|
||||
"BlPresentFlag",
|
||||
"DvBlSignalCompatibilityId"
|
||||
};
|
||||
|
||||
private static readonly string _mediaStreamSaveColumnsInsertQuery =
|
||||
@@ -342,7 +349,7 @@ namespace Emby.Server.Implementations.Data
|
||||
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
|
||||
{
|
||||
const string CreateMediaStreamsTableCommand
|
||||
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))";
|
||||
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, PRIMARY KEY (ItemId, StreamIndex))";
|
||||
const string CreateMediaAttachmentsTableCommand
|
||||
= "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
|
||||
|
||||
@@ -556,6 +563,15 @@ namespace Emby.Server.Implementations.Data
|
||||
AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
|
||||
|
||||
AddColumn(db, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "DvProfile", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "DvLevel", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
|
||||
},
|
||||
TransactionMode);
|
||||
|
||||
@@ -2404,7 +2420,7 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
|
||||
// genres, tags, studios, person, year?
|
||||
builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId))");
|
||||
builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))");
|
||||
|
||||
if (item is MusicArtist)
|
||||
{
|
||||
@@ -3059,12 +3075,12 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "(select CleanValue from itemvalues where ItemId=Guid and Type=0 LIMIT 1)";
|
||||
return "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)";
|
||||
}
|
||||
|
||||
if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "(select CleanValue from itemvalues where ItemId=Guid and Type=1 LIMIT 1)";
|
||||
return "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)";
|
||||
}
|
||||
|
||||
if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -3074,7 +3090,7 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "(select CleanValue from itemvalues where ItemId=Guid and Type=3 LIMIT 1)";
|
||||
return "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)";
|
||||
}
|
||||
|
||||
if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -3147,6 +3163,11 @@ namespace Emby.Server.Implementations.Data
|
||||
return ItemSortBy.IndexNumber;
|
||||
}
|
||||
|
||||
if (string.Equals(name, ItemSortBy.SimilarityScore, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ItemSortBy.SimilarityScore;
|
||||
}
|
||||
|
||||
// Unknown SortBy, just sort by the SortName.
|
||||
return ItemSortBy.SortName;
|
||||
}
|
||||
@@ -3520,6 +3541,13 @@ namespace Emby.Server.Implementations.Data
|
||||
statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
|
||||
}
|
||||
|
||||
if (query.MinParentAndIndexNumber.HasValue)
|
||||
{
|
||||
whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)");
|
||||
statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber);
|
||||
statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber);
|
||||
}
|
||||
|
||||
if (query.MinDateCreated.HasValue)
|
||||
{
|
||||
whereClauses.Add("DateCreated>=@MinDateCreated");
|
||||
@@ -3847,7 +3875,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@ArtistIds" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
@@ -3868,7 +3896,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@ArtistIds" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
@@ -3889,7 +3917,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@ArtistIds" + index;
|
||||
|
||||
clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type=1))");
|
||||
clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
@@ -3931,7 +3959,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@ExcludeArtistId" + index;
|
||||
|
||||
clauses.Add("(guid not in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||
clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
@@ -3952,7 +3980,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@GenreId" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind(paramName, genreId);
|
||||
@@ -3971,7 +3999,7 @@ namespace Emby.Server.Implementations.Data
|
||||
var index = 0;
|
||||
foreach (var item in query.Genres)
|
||||
{
|
||||
clauses.Add("@Genre" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=2)");
|
||||
clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@Genre" + index, GetCleanValue(item));
|
||||
@@ -3990,7 +4018,7 @@ namespace Emby.Server.Implementations.Data
|
||||
var index = 0;
|
||||
foreach (var item in tags)
|
||||
{
|
||||
clauses.Add("@Tag" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=4)");
|
||||
clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@Tag" + index, GetCleanValue(item));
|
||||
@@ -4009,7 +4037,7 @@ namespace Emby.Server.Implementations.Data
|
||||
var index = 0;
|
||||
foreach (var item in excludeTags)
|
||||
{
|
||||
clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from itemvalues where ItemId=Guid and Type=4)");
|
||||
clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
|
||||
@@ -4030,7 +4058,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@StudioId" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
|
||||
|
||||
if (statement != null)
|
||||
{
|
||||
@@ -4509,7 +4537,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
int index = 0;
|
||||
string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++));
|
||||
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
||||
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -4744,11 +4772,11 @@ namespace Emby.Server.Implementations.Data
|
||||
';',
|
||||
new string[]
|
||||
{
|
||||
"delete from itemvalues where type = 6",
|
||||
"delete from ItemValues where type = 6",
|
||||
|
||||
"insert into itemvalues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4",
|
||||
"insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4",
|
||||
|
||||
@"insert into itemvalues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
|
||||
@"insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
|
||||
FROM AncestorIds
|
||||
LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
|
||||
where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 "
|
||||
@@ -4913,6 +4941,7 @@ SELECT key FROM UserDatas WHERE isFavorite=@IsFavorite AND userId=@UserId)
|
||||
AND Type = @InternalPersonType)");
|
||||
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
|
||||
statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
|
||||
statement?.TryBind("@UserId", query.User.InternalId);
|
||||
}
|
||||
|
||||
if (!query.ItemId.Equals(default))
|
||||
@@ -4967,11 +4996,6 @@ AND Type = @InternalPersonType)");
|
||||
statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
|
||||
}
|
||||
|
||||
if (query.User != null)
|
||||
{
|
||||
statement?.TryBind("@UserId", query.User.InternalId);
|
||||
}
|
||||
|
||||
return whereClauses;
|
||||
}
|
||||
|
||||
@@ -5763,7 +5787,7 @@ AND Type = @InternalPersonType)");
|
||||
{
|
||||
var itemIdBlob = id.ToByteArray();
|
||||
|
||||
// First delete chapters
|
||||
// Delete existing mediastreams
|
||||
db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob);
|
||||
|
||||
InsertMediaStreams(itemIdBlob, streams, db);
|
||||
@@ -5855,6 +5879,15 @@ AND Type = @InternalPersonType)");
|
||||
statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries);
|
||||
statement.TryBind("@ColorSpace" + index, stream.ColorSpace);
|
||||
statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer);
|
||||
|
||||
statement.TryBind("@DvVersionMajor" + index, stream.DvVersionMajor);
|
||||
statement.TryBind("@DvVersionMinor" + index, stream.DvVersionMinor);
|
||||
statement.TryBind("@DvProfile" + index, stream.DvProfile);
|
||||
statement.TryBind("@DvLevel" + index, stream.DvLevel);
|
||||
statement.TryBind("@RpuPresentFlag" + index, stream.RpuPresentFlag);
|
||||
statement.TryBind("@ElPresentFlag" + index, stream.ElPresentFlag);
|
||||
statement.TryBind("@BlPresentFlag" + index, stream.BlPresentFlag);
|
||||
statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId);
|
||||
}
|
||||
|
||||
statement.Reset();
|
||||
@@ -5867,10 +5900,10 @@ AND Type = @InternalPersonType)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chapter.
|
||||
/// Gets the media stream.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <returns>ChapterInfo.</returns>
|
||||
/// <returns>MediaStream.</returns>
|
||||
private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader)
|
||||
{
|
||||
var item = new MediaStream
|
||||
@@ -6026,6 +6059,46 @@ AND Type = @InternalPersonType)");
|
||||
item.ColorTransfer = colorTransfer;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(35, out var dvVersionMajor))
|
||||
{
|
||||
item.DvVersionMajor = dvVersionMajor;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(36, out var dvVersionMinor))
|
||||
{
|
||||
item.DvVersionMinor = dvVersionMinor;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(37, out var dvProfile))
|
||||
{
|
||||
item.DvProfile = dvProfile;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(38, out var dvLevel))
|
||||
{
|
||||
item.DvLevel = dvLevel;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(39, out var rpuPresentFlag))
|
||||
{
|
||||
item.RpuPresentFlag = rpuPresentFlag;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(40, out var elPresentFlag))
|
||||
{
|
||||
item.ElPresentFlag = elPresentFlag;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(41, out var blPresentFlag))
|
||||
{
|
||||
item.BlPresentFlag = blPresentFlag;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
|
||||
{
|
||||
item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
|
||||
}
|
||||
|
||||
if (item.Type == MediaStreamType.Subtitle)
|
||||
{
|
||||
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
||||
|
||||
@@ -182,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))
|
||||
@@ -503,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
|
||||
@@ -560,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);
|
||||
@@ -1094,7 +1098,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
{
|
||||
if (item is IHasTrailers hasTrailers)
|
||||
{
|
||||
dto.LocalTrailerCount = hasTrailers.GetTrailerCount();
|
||||
dto.LocalTrailerCount = hasTrailers.LocalTrailers.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -24,15 +24,15 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
||||
<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.3" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.2" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.3" />
|
||||
<PackageReference Include="sharpcompress" Version="0.30.1" />
|
||||
<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>
|
||||
|
||||
@@ -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,6 +36,8 @@ 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>
|
||||
@@ -244,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1860,7 +1860,9 @@ namespace Emby.Server.Implementations.Library
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
}
|
||||
|
||||
var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
|
||||
var outdated = forceUpdate
|
||||
? item.ImageInfos.Where(i => i.Path != null).ToArray()
|
||||
: item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
|
||||
// Skip image processing if current or live tv source
|
||||
if (outdated.Length == 0 || item.SourceType != SourceType.Library)
|
||||
{
|
||||
@@ -1883,7 +1885,7 @@ namespace Emby.Server.Implementations.Library
|
||||
_logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException || ex is IOException)
|
||||
catch (Exception ex) when (ex is InvalidOperationException or IOException)
|
||||
{
|
||||
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
|
||||
continue;
|
||||
@@ -1895,23 +1897,24 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
ImageDimensions size;
|
||||
try
|
||||
{
|
||||
ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
|
||||
size = _imageProcessor.GetImageDimensions(item, image);
|
||||
image.Width = size.Width;
|
||||
image.Height = size.Height;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
||||
size = new ImageDimensions(0, 0);
|
||||
image.Width = 0;
|
||||
image.Height = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path);
|
||||
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -2450,6 +2453,12 @@ namespace Emby.Server.Implementations.Library
|
||||
return RootFolder;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void QueueLibraryScan()
|
||||
{
|
||||
_taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? GetSeasonNumberFromPath(string path)
|
||||
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
|
||||
@@ -2757,7 +2766,8 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public List<Person> GetPeopleItems(InternalPeopleQuery query)
|
||||
{
|
||||
return _itemRepository.GetPeopleNames(query).Select(i =>
|
||||
return _itemRepository.GetPeopleNames(query)
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -2768,7 +2778,12 @@ namespace Emby.Server.Implementations.Library
|
||||
_logger.LogError(ex, "Error getting person");
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null).ToList();
|
||||
})
|
||||
.Where(i => i != null)
|
||||
.Where(i => query.User == null ?
|
||||
true :
|
||||
i.IsVisible(query.User))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public List<string> GetPeopleNames(InternalPeopleQuery query)
|
||||
@@ -2840,10 +2855,12 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var existingNameCount = 1; // first numbered name will be 2
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, name);
|
||||
var originalName = name;
|
||||
while (Directory.Exists(virtualFolderPath))
|
||||
{
|
||||
existingNameCount++;
|
||||
virtualFolderPath = Path.Combine(rootFolderPath, name + " " + existingNameCount);
|
||||
name = originalName + existingNameCount;
|
||||
virtualFolderPath = Path.Combine(rootFolderPath, name);
|
||||
}
|
||||
|
||||
var mediaPathInfos = options.PathInfos;
|
||||
|
||||
@@ -151,7 +151,11 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
|
||||
|
||||
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video))
|
||||
// If file is strm or main media stream is missing, force a metadata refresh with remote probing
|
||||
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
|
||||
&& (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase)
|
||||
|| (item.MediaType == MediaType.Video && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Video))
|
||||
|| (item.MediaType == MediaType.Audio && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio))))
|
||||
{
|
||||
await item.RefreshMetadata(
|
||||
new MetadataRefreshOptions(_directoryService)
|
||||
|
||||
@@ -13,11 +13,11 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
public static int? GetDefaultAudioStreamIndex(IReadOnlyList<MediaStream> streams, IReadOnlyList<string> preferredLanguages, bool preferDefaultTrack)
|
||||
{
|
||||
var sortedStreams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages);
|
||||
var sortedStreams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages).ToList();
|
||||
|
||||
if (preferDefaultTrack)
|
||||
{
|
||||
var defaultStream = streams.FirstOrDefault(i => i.IsDefault);
|
||||
var defaultStream = sortedStreams.FirstOrDefault(i => i.IsDefault);
|
||||
|
||||
if (defaultStream != null)
|
||||
{
|
||||
|
||||
@@ -225,7 +225,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveVideos<Episode>(parent, files, true, collectionType, true);
|
||||
return ResolveVideos<Episode>(parent, files, false, collectionType, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -183,6 +183,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
private static void SetProviderIdFromPath(Series item, string path)
|
||||
{
|
||||
var justName = Path.GetFileName(path.AsSpan());
|
||||
|
||||
var imdbId = justName.GetAttributeValue("imdbid");
|
||||
if (!string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
item.SetProviderId(MetadataProvider.Imdb, imdbId);
|
||||
}
|
||||
|
||||
var tvdbId = justName.GetAttributeValue("tvdbid");
|
||||
if (!string.IsNullOrEmpty(tvdbId))
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
await RecordFromFile(mediaSource, mediaSource.Path, targetFile, onStarted, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Recording completed to file {0}", targetFile);
|
||||
_logger.LogInformation("Recording completed to file {Path}", targetFile);
|
||||
}
|
||||
|
||||
private async Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, Action onStarted, CancellationToken cancellationToken)
|
||||
@@ -115,7 +115,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
|
||||
_ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
|
||||
|
||||
_logger.LogInformation("ffmpeg recording process started for {0}", _targetPath);
|
||||
_logger.LogInformation("ffmpeg recording process started for {Path}", _targetPath);
|
||||
|
||||
// Block until ffmpeg exits
|
||||
await _taskCompletionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using System.Text;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
@@ -48,12 +49,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(info.EpisodeTitle))
|
||||
{
|
||||
var tmpName = name;
|
||||
if (addHyphen)
|
||||
{
|
||||
name += " -";
|
||||
tmpName += " -";
|
||||
}
|
||||
|
||||
name += " " + info.EpisodeTitle;
|
||||
tmpName += " " + info.EpisodeTitle;
|
||||
// Since the filename will be used with file ext. (.mp4, .ts, etc)
|
||||
if (Encoding.UTF8.GetByteCount(tmpName) < 250)
|
||||
{
|
||||
name = tmpName;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (info.IsMovie && info.ProductionYear != null)
|
||||
|
||||
@@ -165,12 +165,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
|
||||
const double DesiredAspect = 2.0 / 3;
|
||||
|
||||
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ??
|
||||
GetProgramImage(ApiUrl, allImages, DesiredAspect);
|
||||
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect, token) ??
|
||||
GetProgramImage(ApiUrl, allImages, DesiredAspect, token);
|
||||
|
||||
const double WideAspect = 16.0 / 9;
|
||||
|
||||
programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect);
|
||||
programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect, token);
|
||||
|
||||
// Don't supply the same image twice
|
||||
if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
|
||||
@@ -178,7 +178,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
programEntry.ThumbImage = null;
|
||||
}
|
||||
|
||||
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect);
|
||||
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect, token);
|
||||
|
||||
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
|
||||
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
|
||||
@@ -399,7 +399,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return info;
|
||||
}
|
||||
|
||||
private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect)
|
||||
private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect, string token)
|
||||
{
|
||||
var match = images
|
||||
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
|
||||
@@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
else
|
||||
{
|
||||
return apiUrl + "/image/" + uri;
|
||||
return apiUrl + "/image/" + uri + "?token=" + token;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,6 +457,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
IReadOnlyList<string> programIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (programIds.Count == 0)
|
||||
{
|
||||
return Array.Empty<ShowImagesDto>();
|
||||
@@ -478,6 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json)
|
||||
};
|
||||
message.Headers.TryAddWithoutValidation("token", token);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
@@ -26,6 +27,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
public class XmlTvListingsProvider : IListingsProvider
|
||||
{
|
||||
private static readonly TimeSpan _maxCacheAge = TimeSpan.FromHours(1);
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<XmlTvListingsProvider> _logger;
|
||||
@@ -69,13 +72,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return UnzipIfNeeded(info.Path, info.Path);
|
||||
}
|
||||
|
||||
string cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + "-" + info.Id + ".xml";
|
||||
string cacheFilename = info.Id + ".xml";
|
||||
string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
|
||||
if (File.Exists(cacheFile))
|
||||
if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge))
|
||||
{
|
||||
return UnzipIfNeeded(info.Path, cacheFile);
|
||||
}
|
||||
|
||||
// Must check if file exists as parent directory may not exist.
|
||||
if (File.Exists(cacheFile))
|
||||
{
|
||||
File.Delete(cacheFile);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
|
||||
@@ -124,7 +133,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
using (var stream = File.OpenRead(file))
|
||||
{
|
||||
string tempFolder = Path.Combine(_config.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString());
|
||||
string tempFolder = GetTempFolderPath(stream);
|
||||
Directory.CreateDirectory(tempFolder);
|
||||
|
||||
_zipClient.ExtractFirstFileFromGz(stream, tempFolder, "data.xml");
|
||||
@@ -137,7 +146,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
using (var stream = File.OpenRead(file))
|
||||
{
|
||||
string tempFolder = Path.Combine(_config.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString());
|
||||
string tempFolder = GetTempFolderPath(stream);
|
||||
Directory.CreateDirectory(tempFolder);
|
||||
|
||||
_zipClient.ExtractAllFromGz(stream, tempFolder, true);
|
||||
@@ -146,6 +155,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
}
|
||||
|
||||
private string GetTempFolderPath(Stream stream)
|
||||
{
|
||||
#pragma warning disable CA5351
|
||||
using var md5 = MD5.Create();
|
||||
#pragma warning restore CA5351
|
||||
var checksum = Convert.ToHexString(md5.ComputeHash(stream));
|
||||
stream.Position = 0;
|
||||
return Path.Combine(_config.ApplicationPaths.TempDirectory, checksum);
|
||||
}
|
||||
|
||||
private string FindXmlFile(string directory)
|
||||
{
|
||||
return _fileSystem.GetFiles(directory, true)
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none");
|
||||
return VerifyReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), "none");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -386,6 +386,7 @@ namespace Emby.Server.Implementations.Localization
|
||||
yield return new LocalizationOption("Español (Dominicana)", "es_DO");
|
||||
yield return new LocalizationOption("Español (México)", "es-MX");
|
||||
yield return new LocalizationOption("Eesti", "et");
|
||||
yield return new LocalizationOption("Basque", "eu");
|
||||
yield return new LocalizationOption("فارسی", "fa");
|
||||
yield return new LocalizationOption("Suomi", "fi");
|
||||
yield return new LocalizationOption("Filipino", "fil");
|
||||
|
||||
@@ -104,6 +104,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
||||
{
|
||||
_logger.LogError(ex, "Error updating {0}", package.Name);
|
||||
}
|
||||
catch (InvalidDataException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating {0}", package.Name);
|
||||
}
|
||||
|
||||
// Update progress
|
||||
lock (progress)
|
||||
|
||||
@@ -329,13 +329,17 @@ namespace Emby.Server.Implementations.Session
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CloseIfNeeded(SessionInfo session)
|
||||
public async Task CloseIfNeededAsync(SessionInfo session)
|
||||
{
|
||||
if (!session.SessionControllers.Any(i => i.IsSessionActive))
|
||||
{
|
||||
var key = GetSessionKey(session.Client, session.DeviceId);
|
||||
|
||||
_activeConnections.TryRemove(key, out _);
|
||||
if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
|
||||
{
|
||||
await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
OnSessionEnded(session);
|
||||
}
|
||||
@@ -413,6 +417,7 @@ namespace Emby.Server.Implementations.Session
|
||||
session.PlayState.IsPaused = info.IsPaused;
|
||||
session.PlayState.PositionTicks = info.PositionTicks;
|
||||
session.PlayState.MediaSourceId = info.MediaSourceId;
|
||||
session.PlayState.LiveStreamId = info.LiveStreamId;
|
||||
session.PlayState.CanSeek = info.CanSeek;
|
||||
session.PlayState.IsMuted = info.IsMuted;
|
||||
session.PlayState.VolumeLevel = info.VolumeLevel;
|
||||
@@ -699,7 +704,9 @@ namespace Emby.Server.Implementations.Session
|
||||
DeviceName = session.DeviceName,
|
||||
ClientName = session.Client,
|
||||
DeviceId = session.DeviceId,
|
||||
Session = session
|
||||
Session = session,
|
||||
PlaybackPositionTicks = info.PositionTicks,
|
||||
PlaySessionId = info.PlaySessionId
|
||||
};
|
||||
|
||||
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
|
||||
@@ -768,6 +775,11 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
|
||||
{
|
||||
ClearTranscodingInfo(session.DeviceId);
|
||||
}
|
||||
|
||||
var users = GetUsers(session);
|
||||
|
||||
// only update saved user data on actual check-ins, not automated ones
|
||||
@@ -985,7 +997,8 @@ namespace Emby.Server.Implementations.Session
|
||||
DeviceName = session.DeviceName,
|
||||
ClientName = session.Client,
|
||||
DeviceId = session.DeviceId,
|
||||
Session = session
|
||||
Session = session,
|
||||
PlaySessionId = info.PlaySessionId
|
||||
};
|
||||
|
||||
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
|
||||
|
||||
@@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Session
|
||||
{
|
||||
public sealed class WebSocketController : ISessionController, IDisposable
|
||||
public sealed class WebSocketController : ISessionController, IAsyncDisposable, IDisposable
|
||||
{
|
||||
private readonly ILogger<WebSocketController> _logger;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
@@ -53,13 +53,13 @@ namespace Emby.Server.Implementations.Session
|
||||
connection.Closed += OnConnectionClosed;
|
||||
}
|
||||
|
||||
private void OnConnectionClosed(object? sender, EventArgs e)
|
||||
private async void OnConnectionClosed(object? sender, EventArgs e)
|
||||
{
|
||||
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
|
||||
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
|
||||
_sockets.Remove(connection);
|
||||
connection.Closed -= OnConnectionClosed;
|
||||
_sessionManager.CloseIfNeeded(_session);
|
||||
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -99,6 +99,23 @@ namespace Emby.Server.Implementations.Session
|
||||
foreach (var socket in _sockets)
|
||||
{
|
||||
socket.Closed -= OnConnectionClosed;
|
||||
socket.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var socket in _sockets)
|
||||
{
|
||||
socket.Closed -= OnConnectionClosed;
|
||||
await socket.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
@@ -34,6 +34,11 @@ namespace Emby.Server.Implementations.Sorting
|
||||
throw new ArgumentNullException(nameof(y));
|
||||
}
|
||||
|
||||
if (!x.IndexNumber.HasValue && !y.IndexNumber.HasValue)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!x.IndexNumber.HasValue)
|
||||
{
|
||||
return -1;
|
||||
|
||||
@@ -34,6 +34,11 @@ namespace Emby.Server.Implementations.Sorting
|
||||
throw new ArgumentNullException(nameof(y));
|
||||
}
|
||||
|
||||
if (!x.ParentIndexNumber.HasValue && !y.ParentIndexNumber.HasValue)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!x.ParentIndexNumber.HasValue)
|
||||
{
|
||||
return -1;
|
||||
|
||||
@@ -135,20 +135,15 @@ namespace Emby.Server.Implementations.TV
|
||||
return GetResult(episodes, request);
|
||||
}
|
||||
|
||||
public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
|
||||
private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
|
||||
{
|
||||
// Avoid implicitly captured closure
|
||||
var currentUser = user;
|
||||
|
||||
var allNextUp = seriesKeys
|
||||
.Select(i => GetNextUp(i, currentUser, dtoOptions, false));
|
||||
var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false));
|
||||
|
||||
if (request.EnableRewatching)
|
||||
{
|
||||
allNextUp = allNextUp.Concat(
|
||||
seriesKeys.Select(i => GetNextUp(i, currentUser, dtoOptions, true))
|
||||
)
|
||||
.OrderByDescending(i => i.Item1);
|
||||
seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true)))
|
||||
.OrderByDescending(i => i.LastWatchedDate);
|
||||
}
|
||||
|
||||
// If viewing all next up for all series, remove first episodes
|
||||
@@ -161,23 +156,18 @@ namespace Emby.Server.Implementations.TV
|
||||
{
|
||||
if (request.DisableFirstEpisode)
|
||||
{
|
||||
return i.Item1 != DateTime.MinValue;
|
||||
return i.LastWatchedDate != DateTime.MinValue;
|
||||
}
|
||||
|
||||
if (alwaysEnableFirstEpisode || (i.Item1 != DateTime.MinValue && i.Item1.Date >= request.NextUpDateCutoff))
|
||||
if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff))
|
||||
{
|
||||
anyFound = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!anyFound && i.Item1 == DateTime.MinValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return !anyFound && i.LastWatchedDate == DateTime.MinValue;
|
||||
})
|
||||
.Select(i => i.Item2())
|
||||
.Select(i => i.GetEpisodeFunction())
|
||||
.Where(i => i != null);
|
||||
}
|
||||
|
||||
@@ -195,14 +185,13 @@ namespace Emby.Server.Implementations.TV
|
||||
/// Gets the next up.
|
||||
/// </summary>
|
||||
/// <returns>Task{Episode}.</returns>
|
||||
private Tuple<DateTime, Func<Episode>> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
|
||||
private (DateTime LastWatchedDate, Func<Episode> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
|
||||
{
|
||||
var lastQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Descending) },
|
||||
IsPlayed = true,
|
||||
Limit = 1,
|
||||
ParentIndexNumberNotEquals = 0,
|
||||
@@ -213,42 +202,38 @@ namespace Emby.Server.Implementations.TV
|
||||
}
|
||||
};
|
||||
|
||||
if (rewatching)
|
||||
{
|
||||
// find last watched by date played, not by newest episode watched
|
||||
lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) };
|
||||
}
|
||||
// If rewatching is enabled, sort first by date played and then by season and episode numbers
|
||||
lastQuery.OrderBy = rewatching
|
||||
? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
|
||||
: new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
|
||||
|
||||
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
|
||||
|
||||
Func<Episode> getEpisode = () =>
|
||||
Episode GetEpisode()
|
||||
{
|
||||
var nextQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||
OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
|
||||
Limit = 1,
|
||||
IsPlayed = rewatching,
|
||||
IsVirtualItem = false,
|
||||
ParentIndexNumberNotEquals = 0,
|
||||
MinSortName = lastWatchedEpisode?.SortName,
|
||||
DtoOptions = dtoOptions
|
||||
};
|
||||
|
||||
Episode nextEpisode;
|
||||
if (rewatching)
|
||||
// Locate the next up episode based on the last watched episode's season and episode number
|
||||
var lastWatchedParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber;
|
||||
var lastWatchedIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber;
|
||||
if (lastWatchedParentIndexNumber.HasValue && lastWatchedIndexNumber.HasValue)
|
||||
{
|
||||
nextQuery.Limit = 2;
|
||||
// get watched episode after most recently watched
|
||||
nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().ElementAtOrDefault(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
|
||||
nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1);
|
||||
}
|
||||
|
||||
var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
|
||||
|
||||
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
|
||||
{
|
||||
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
@@ -297,7 +282,7 @@ namespace Emby.Server.Implementations.TV
|
||||
}
|
||||
|
||||
return nextEpisode;
|
||||
};
|
||||
}
|
||||
|
||||
if (lastWatchedEpisode != null)
|
||||
{
|
||||
@@ -305,11 +290,11 @@ namespace Emby.Server.Implementations.TV
|
||||
|
||||
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
|
||||
|
||||
return new Tuple<DateTime, Func<Episode>>(lastWatchedDate, getEpisode);
|
||||
return (lastWatchedDate, GetEpisode);
|
||||
}
|
||||
|
||||
// Return the first episode
|
||||
return new Tuple<DateTime, Func<Episode>>(DateTime.MinValue, getEpisode);
|
||||
return (DateTime.MinValue, GetEpisode);
|
||||
}
|
||||
|
||||
private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)
|
||||
|
||||
@@ -324,9 +324,15 @@ namespace Emby.Server.Implementations.Updates
|
||||
}
|
||||
|
||||
_completedInstallationsInternal.Add(package);
|
||||
await _eventManager.PublishAsync(isUpdate
|
||||
? (GenericEventArgs<InstallationInfo>)new PluginUpdatedEventArgs(package)
|
||||
: new PluginInstalledEventArgs(package)).ConfigureAwait(false);
|
||||
|
||||
if (isUpdate)
|
||||
{
|
||||
await _eventManager.PublishAsync(new PluginUpdatedEventArgs(package)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _eventManager.PublishAsync(new PluginInstalledEventArgs(package)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_applicationHost.NotifyPendingRestart();
|
||||
}
|
||||
|
||||
25
Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs
Normal file
25
Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Emby.Dlna;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Jellyfin.Api.Attributes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class DlnaEnabledAttribute : ActionFilterAttribute
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var serverConfigurationManager = context.HttpContext.RequestServices.GetRequiredService<IServerConfigurationManager>();
|
||||
|
||||
var enabled = serverConfigurationManager.GetDlnaConfiguration().EnableServer;
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
context.Result = new StatusCodeResult(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,12 +43,16 @@ namespace Jellyfin.Api.Auth
|
||||
/// <param name="ignoreSchedule">Whether to ignore parental control.</param>
|
||||
/// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
|
||||
/// <param name="requiredDownloadPermission">Whether validation requires download permission.</param>
|
||||
/// <param name="requireLiveTvManagementPermission">Whether validation requires LiveTV management permission.</param>
|
||||
/// <param name="requireLiveTvAccessPermission">Whether validation requires LiveTV management permission.</param>
|
||||
/// <returns>Validated claim status.</returns>
|
||||
protected bool ValidateClaims(
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
bool ignoreSchedule = false,
|
||||
bool localAccessOnly = false,
|
||||
bool requiredDownloadPermission = false)
|
||||
bool requiredDownloadPermission = false,
|
||||
bool requireLiveTvManagementPermission = false,
|
||||
bool requireLiveTvAccessPermission = false)
|
||||
{
|
||||
// ApiKey is currently global admin, always allow.
|
||||
var isApiKey = ClaimHelpers.GetIsApiKey(claimsPrincipal);
|
||||
@@ -106,6 +110,20 @@ namespace Jellyfin.Api.Auth
|
||||
return false;
|
||||
}
|
||||
|
||||
// User attempting to access LiveTV without permission.
|
||||
if (requireLiveTvAccessPermission
|
||||
&& !user.HasPermission(PermissionKind.EnableLiveTvAccess))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// User attempting to manage LiveTV without permission.
|
||||
if (requireLiveTvManagementPermission
|
||||
&& !user.HasPermission(PermissionKind.EnableLiveTvManagement))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Jellyfin.Api.Auth.DownloadPolicy
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User);
|
||||
var validated = ValidateClaims(context.User, requiredDownloadPermission: true);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
|
||||
43
Jellyfin.Api/Auth/LiveTvAccessPolicy/LiveTvAccessHandler.cs
Normal file
43
Jellyfin.Api/Auth/LiveTvAccessPolicy/LiveTvAccessHandler.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LiveTvAccessPolicy;
|
||||
|
||||
/// <summary>
|
||||
/// Authorization handler for LiveTV access.
|
||||
/// </summary>
|
||||
public class LiveTvAccessHandler : BaseAuthorizationHandler<LiveTvAccessRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LiveTvAccessHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public LiveTvAccessHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LiveTvAccessRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User, requireLiveTvAccessPermission: true);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LiveTvAccessPolicy;
|
||||
|
||||
/// <summary>
|
||||
/// The LiveTV access requirement.
|
||||
/// </summary>
|
||||
public class LiveTvAccessRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LiveTvManagementPolicy;
|
||||
|
||||
/// <summary>
|
||||
/// Authorization handler for LiveTV management access.
|
||||
/// </summary>
|
||||
public class LiveTvManagementHandler : BaseAuthorizationHandler<LiveTvManagementRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LiveTvManagementHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public LiveTvManagementHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LiveTvManagementRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User, requireLiveTvManagementPermission: true);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LiveTvManagementPolicy;
|
||||
|
||||
/// <summary>
|
||||
/// The LiveTV management requirement.
|
||||
/// </summary>
|
||||
public class LiveTvManagementRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
@@ -74,5 +74,15 @@ namespace Jellyfin.Api.Constants
|
||||
/// Policy name for accessing a SyncPlay group.
|
||||
/// </summary>
|
||||
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for accessing LiveTV.
|
||||
/// </summary>
|
||||
public const string LiveTvAccess = "LiveTvAccess";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for managing LiveTV.
|
||||
/// </summary>
|
||||
public const string LiveTvManagement = "LiveTvManagement";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,18 +91,18 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetAudioStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -132,8 +132,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -262,12 +262,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -297,8 +297,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
|
||||
@@ -86,21 +86,23 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Updates named configuration.
|
||||
/// </summary>
|
||||
/// <param name="key">Configuration key.</param>
|
||||
/// <param name="configuration">Configuration.</param>
|
||||
/// <response code="204">Named configuration updated.</response>
|
||||
/// <returns>Update status.</returns>
|
||||
[HttpPost("Configuration/{key}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> UpdateNamedConfiguration([FromRoute, Required] string key)
|
||||
public ActionResult UpdateNamedConfiguration([FromRoute, Required] string key, [FromBody, Required] JsonDocument configuration)
|
||||
{
|
||||
var configurationType = _configurationManager.GetConfigurationType(key);
|
||||
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false);
|
||||
if (configuration == null)
|
||||
var deserializedConfiguration = configuration.Deserialize(configurationType, _serializerOptions);
|
||||
|
||||
if (deserializedConfiguration == null)
|
||||
{
|
||||
throw new ArgumentException("Body doesn't contain a valid configuration");
|
||||
}
|
||||
|
||||
_configurationManager.SaveConfiguration(key, configuration);
|
||||
_configurationManager.SaveConfiguration(key, deserializedConfiguration);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -123,12 +125,14 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
|
||||
/// <response code="204">Media encoder path updated.</response>
|
||||
/// <returns>Status.</returns>
|
||||
[Obsolete("This endpoint is obsolete.")]
|
||||
[HttpPost("MediaEncoder/Path")]
|
||||
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
|
||||
{
|
||||
_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
|
||||
// API ENDPOINT DISABLED (NOOP) FOR SECURITY PURPOSES
|
||||
//_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Dlna Server Controller.
|
||||
/// </summary>
|
||||
[Route("Dlna")]
|
||||
[DlnaEnabled]
|
||||
[Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
|
||||
public class DlnaServerController : BaseJellyfinApiController
|
||||
{
|
||||
@@ -55,15 +56,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
var url = GetAbsoluteUri();
|
||||
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
||||
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
|
||||
return Ok(xml);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
var url = GetAbsoluteUri();
|
||||
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
||||
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
|
||||
return Ok(xml);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -83,12 +79,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return Ok(_contentDirectory.GetServiceXml());
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return Ok(_contentDirectory.GetServiceXml());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -108,12 +99,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -133,12 +119,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return Ok(_connectionManager.GetServiceXml());
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return Ok(_connectionManager.GetServiceXml());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -155,12 +136,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -177,12 +153,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -199,12 +170,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -224,12 +190,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return ProcessEventRequest(_mediaReceiverRegistrar);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return ProcessEventRequest(_mediaReceiverRegistrar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -249,12 +210,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return ProcessEventRequest(_contentDirectory);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return ProcessEventRequest(_contentDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -274,12 +230,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return ProcessEventRequest(_connectionManager);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return ProcessEventRequest(_connectionManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -299,12 +250,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesImageFile]
|
||||
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return GetIconInternal(fileName);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return GetIconInternal(fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -322,12 +268,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesImageFile]
|
||||
public ActionResult GetIcon([FromRoute, Required] string fileName)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return GetIconInternal(fileName);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return GetIconInternal(fileName);
|
||||
}
|
||||
|
||||
private ActionResult GetIconInternal(string fileName)
|
||||
|
||||
@@ -13,6 +13,7 @@ using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.PlaybackDtos;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.MediaEncoding.Hls.Playlist;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -21,6 +22,7 @@ using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.MediaEncoding.Encoder;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -172,18 +174,18 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesPlaylistFile]
|
||||
public async Task<ActionResult> GetLiveHlsStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -213,8 +215,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -285,7 +287,7 @@ namespace Jellyfin.Api.Controllers
|
||||
// Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
|
||||
// since it gets disposed when ffmpeg exits
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
using var state = await StreamingHelpers.GetStreamingState(
|
||||
var state = await StreamingHelpers.GetStreamingState(
|
||||
streamingRequest,
|
||||
Request,
|
||||
_authContext,
|
||||
@@ -424,12 +426,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery, Required] string mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -461,8 +463,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -594,12 +596,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery, Required] string mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -630,8 +632,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -760,12 +762,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -797,8 +799,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -928,12 +930,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -964,8 +966,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -1105,12 +1107,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -1142,8 +1144,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -1286,12 +1288,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -1322,8 +1324,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -1414,7 +1416,8 @@ namespace Jellyfin.Api.Controllers
|
||||
state.RunTimeTicks ?? 0,
|
||||
state.Request.SegmentContainer ?? string.Empty,
|
||||
"hls1/main/",
|
||||
Request.QueryString.ToString());
|
||||
Request.QueryString.ToString(),
|
||||
EncodingHelper.IsCopyCodec(state.OutputVideoCodec));
|
||||
var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
|
||||
|
||||
return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
|
||||
@@ -1431,7 +1434,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
using var state = await StreamingHelpers.GetStreamingState(
|
||||
var state = await StreamingHelpers.GetStreamingState(
|
||||
streamingRequest,
|
||||
Request,
|
||||
_authContext,
|
||||
@@ -1661,8 +1664,8 @@ namespace Jellyfin.Api.Controllers
|
||||
startNumber.ToString(CultureInfo.InvariantCulture),
|
||||
baseUrlParam,
|
||||
isEventPlaylist ? "event" : "vod",
|
||||
outputTsArg,
|
||||
outputPath).Trim();
|
||||
EncodingUtils.NormalizePath(outputTsArg),
|
||||
EncodingUtils.NormalizePath(outputPath)).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1692,7 +1695,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
audioTranscodeParams += "-acodec " + audioCodec;
|
||||
|
||||
if (state.OutputAudioBitrate.HasValue)
|
||||
if (state.OutputAudioBitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(state.ActualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -1711,20 +1714,32 @@ namespace Jellyfin.Api.Controllers
|
||||
return audioTranscodeParams;
|
||||
}
|
||||
|
||||
// dts, flac, opus and truehd are experimental in mp4 muxer
|
||||
var strictArgs = string.Empty;
|
||||
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
|
||||
if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
strictArgs = " -strict -2";
|
||||
}
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
|
||||
{
|
||||
return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0" + bitStreamArgs;
|
||||
return copyArgs + " -copypriorss:a:0 0";
|
||||
}
|
||||
|
||||
return "-codec:a:0 copy -strict -2" + bitStreamArgs;
|
||||
return copyArgs;
|
||||
}
|
||||
|
||||
var args = "-codec:a:0 " + audioCodec;
|
||||
var args = "-codec:a:0 " + audioCodec + strictArgs;
|
||||
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
@@ -1734,8 +1749,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
var bitrate = state.OutputAudioBitrate;
|
||||
|
||||
if (bitrate.HasValue)
|
||||
if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -1773,13 +1787,25 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var args = "-codec:v:0 " + codec;
|
||||
|
||||
// Prefer hvc1 to hev1.
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -tag:v:0 hvc1";
|
||||
if (EncodingHelper.IsCopyCodec(codec)
|
||||
&& (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// Prefer dvh1 to dvhe
|
||||
args += " -tag:v:0 dvh1 -strict -2";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prefer hvc1 to hev1
|
||||
args += " -tag:v:0 hvc1";
|
||||
}
|
||||
}
|
||||
|
||||
// if (state.EnableMpegtsM2TsMode)
|
||||
@@ -1818,7 +1844,11 @@ namespace Jellyfin.Api.Controllers
|
||||
// args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
|
||||
|
||||
// video processing filters.
|
||||
args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
|
||||
var videoProcessParam = _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
|
||||
|
||||
var negativeMapArgs = _encodingHelper.GetNegativeMapArgsByFilters(state, videoProcessParam);
|
||||
|
||||
args = negativeMapArgs + args + videoProcessParam;
|
||||
|
||||
// -start_at_zero is necessary to use with -ss when seeking,
|
||||
// otherwise the target position cannot be determined.
|
||||
|
||||
@@ -1724,6 +1724,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery, Range(0, 100)] int quality = 90)
|
||||
{
|
||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||
if (!brandingOptions.SplashscreenEnabled)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
string splashscreenPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
|
||||
@@ -1776,6 +1781,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a custom splashscreen.
|
||||
/// The body is expected to the image contents base64 encoded.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
/// <response code="204">Successfully uploaded new splashscreen.</response>
|
||||
@@ -1799,7 +1805,13 @@ namespace Jellyfin.Api.Controllers
|
||||
return BadRequest("Error reading mimetype from uploaded image");
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType.Value));
|
||||
var extension = MimeTypes.ToExtension(mimeType.Value);
|
||||
if (string.IsNullOrEmpty(extension))
|
||||
{
|
||||
return BadRequest("Error converting mimetype to an image extension");
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
|
||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||
brandingOptions.SplashscreenLocation = filePath;
|
||||
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
|
||||
@@ -1812,6 +1824,29 @@ namespace Jellyfin.Api.Controllers
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a custom splashscreen.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
/// <response code="204">Successfully deleted the custom splashscreen.</response>
|
||||
/// <response code="403">User does not have permission to delete splashscreen..</response>
|
||||
[HttpDelete("Branding/Splashscreen")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DeleteCustomSplashscreen()
|
||||
{
|
||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||
if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation)
|
||||
&& System.IO.File.Exists(brandingOptions.SplashscreenLocation))
|
||||
{
|
||||
System.IO.File.Delete(brandingOptions.SplashscreenLocation);
|
||||
brandingOptions.SplashscreenLocation = null;
|
||||
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
|
||||
{
|
||||
using var reader = new StreamReader(inputStream);
|
||||
|
||||
@@ -89,6 +89,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="isMovie">Optional filter for live tv movies.</param>
|
||||
/// <param name="isSeries">Optional filter for live tv series.</param>
|
||||
/// <param name="isNews">Optional filter for live tv news.</param>
|
||||
/// <param name="isKids">Optional filter for live tv kids.</param>
|
||||
/// <param name="isSports">Optional filter for live tv sports.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
@@ -173,6 +178,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSeries,
|
||||
[FromQuery] bool? isNews,
|
||||
[FromQuery] bool? isKids,
|
||||
[FromQuery] bool? isSports,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
@@ -260,30 +270,13 @@ namespace Jellyfin.Api.Controllers
|
||||
includeItemTypes = new[] { BaseItemKind.Playlist };
|
||||
}
|
||||
|
||||
var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
|
||||
|
||||
bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1
|
||||
// Assume all folders inside an EnabledChannel are enabled
|
||||
|| Array.IndexOf(enabledChannels, item.Id) != -1
|
||||
// Assume all items inside an EnabledChannel are enabled
|
||||
|| Array.IndexOf(enabledChannels, item.ChannelId) != -1;
|
||||
|
||||
var collectionFolders = _libraryManager.GetCollectionFolders(item);
|
||||
foreach (var collectionFolder in collectionFolders)
|
||||
{
|
||||
if (user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
|
||||
{
|
||||
isInEnabledFolder = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (item is not UserRootFolder
|
||||
&& !isInEnabledFolder
|
||||
&& !user.HasPermission(PermissionKind.EnableAllFolders)
|
||||
&& !user.HasPermission(PermissionKind.EnableAllChannels)
|
||||
&& !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
|
||||
// api keys can always access all folders
|
||||
&& !ClaimHelpers.GetIsApiKey(User)
|
||||
// check the item is visible for the user
|
||||
&& !item.IsVisible(user))
|
||||
{
|
||||
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
|
||||
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user!.Username, item.Name);
|
||||
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
|
||||
}
|
||||
|
||||
@@ -316,6 +309,11 @@ namespace Jellyfin.Api.Controllers
|
||||
Is3D = is3D,
|
||||
HasTvdbId = hasTvdbId,
|
||||
HasTmdbId = hasTmdbId,
|
||||
IsMovie = isMovie,
|
||||
IsSeries = isSeries,
|
||||
IsNews = isNews,
|
||||
IsKids = isKids,
|
||||
IsSports = isSports,
|
||||
HasOverview = hasOverview,
|
||||
HasOfficialRating = hasOfficialRating,
|
||||
HasParentalRating = hasParentalRating,
|
||||
@@ -515,8 +513,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
|
||||
/// <param name="isHd">Optional filter by items that are HD or not.</param>
|
||||
/// <param name="is4K">Optional filter by items that are 4K or not.</param>
|
||||
/// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
|
||||
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
|
||||
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
|
||||
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
@@ -529,42 +527,47 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="isMovie">Optional filter for live tv movies.</param>
|
||||
/// <param name="isSeries">Optional filter for live tv series.</param>
|
||||
/// <param name="isNews">Optional filter for live tv news.</param>
|
||||
/// <param name="isKids">Optional filter for live tv kids.</param>
|
||||
/// <param name="isSports">Optional filter for live tv sports.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
|
||||
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
|
||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
|
||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
|
||||
/// <param name="isPlayed">Optional filter by items that are played, or not.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
|
||||
/// <param name="enableUserData">Optional, include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
|
||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
|
||||
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
|
||||
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
|
||||
/// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
|
||||
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
|
||||
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
|
||||
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
|
||||
/// <param name="isLocked">Optional filter by items that are locked.</param>
|
||||
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
|
||||
@@ -575,12 +578,12 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
|
||||
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
|
||||
/// <param name="is3D">Optional filter by items that are 3D, or not.</param>
|
||||
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
|
||||
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
||||
@@ -613,6 +616,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSeries,
|
||||
[FromQuery] bool? isNews,
|
||||
[FromQuery] bool? isKids,
|
||||
[FromQuery] bool? isSports,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
@@ -695,6 +703,11 @@ namespace Jellyfin.Api.Controllers
|
||||
hasImdbId,
|
||||
hasTmdbId,
|
||||
hasTvdbId,
|
||||
isMovie,
|
||||
isSeries,
|
||||
isNews,
|
||||
isKids,
|
||||
isSports,
|
||||
excludeItemIds,
|
||||
startIndex,
|
||||
limit,
|
||||
|
||||
@@ -492,7 +492,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Media folders returned.</response>
|
||||
/// <returns>List of user media folders.</returns>
|
||||
[HttpGet("Library/MediaFolders")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
|
||||
{
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Info")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<LiveTvInfo> GetLiveTvInfo()
|
||||
{
|
||||
return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
|
||||
@@ -129,7 +129,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Channels")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
|
||||
[FromQuery] ChannelType? type,
|
||||
[FromQuery] Guid? userId,
|
||||
@@ -208,7 +208,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
|
||||
[HttpGet("Channels/{channelId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
|
||||
{
|
||||
var user = userId is null || userId.Value.Equals(default)
|
||||
@@ -249,7 +249,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
|
||||
[HttpGet("Recordings")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
|
||||
[FromQuery] string? channelId,
|
||||
[FromQuery] Guid? userId,
|
||||
@@ -320,7 +320,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
|
||||
[HttpGet("Recordings/Series")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[Obsolete("This endpoint is obsolete.")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
|
||||
@@ -363,7 +363,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
|
||||
[HttpGet("Recordings/Groups")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[Obsolete("This endpoint is obsolete.")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
|
||||
@@ -379,7 +379,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
|
||||
[HttpGet("Recordings/Folders")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
|
||||
{
|
||||
var user = userId is null || userId.Value.Equals(default)
|
||||
@@ -401,7 +401,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
|
||||
[HttpGet("Recordings/{recordingId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
|
||||
{
|
||||
var user = userId is null || userId.Value.Equals(default)
|
||||
@@ -423,10 +423,9 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Tuners/{tunerId}/Reset")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -441,7 +440,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Timers/{timerId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
|
||||
{
|
||||
return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -457,7 +456,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Timers/Defaults")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
|
||||
{
|
||||
return string.IsNullOrEmpty(programId)
|
||||
@@ -477,7 +476,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Timers")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
|
||||
[FromQuery] string? channelId,
|
||||
[FromQuery] string? seriesTimerId,
|
||||
@@ -531,7 +530,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Programs")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
|
||||
[FromQuery] Guid? userId,
|
||||
@@ -614,7 +613,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpPost("Programs")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
|
||||
{
|
||||
var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
|
||||
@@ -680,7 +679,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Recommended epgs returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
|
||||
[HttpGet("Programs/Recommended")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
|
||||
[FromQuery] Guid? userId,
|
||||
@@ -732,7 +731,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Program returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
|
||||
[HttpGet("Programs/{programId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<BaseItemDto>> GetProgram(
|
||||
[FromRoute, Required] string programId,
|
||||
@@ -753,13 +752,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpDelete("Recordings/{recordingId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteRecording([FromRoute, Required] Guid recordingId)
|
||||
public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
|
||||
var item = _libraryManager.GetItemById(recordingId);
|
||||
if (item == null)
|
||||
{
|
||||
@@ -781,11 +778,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Timer deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("Timers/{timerId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -798,12 +794,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Timer updated.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Timers/{timerId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -815,11 +810,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Timer created.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Timers")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -832,7 +826,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="404">Series timer not found.</response>
|
||||
/// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
|
||||
[HttpGet("SeriesTimers/{timerId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
|
||||
@@ -854,7 +848,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Timers returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
|
||||
[HttpGet("SeriesTimers")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
|
||||
{
|
||||
@@ -874,11 +868,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Timer cancelled.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("SeriesTimers/{timerId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -891,12 +884,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Series timer updated.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("SeriesTimers/{timerId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -908,11 +900,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Series timer info created.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("SeriesTimers")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -923,7 +914,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="groupId">Group id.</param>
|
||||
/// <returns>A <see cref="NotFoundResult"/>.</returns>
|
||||
[HttpGet("Recordings/Groups/{groupId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("This endpoint is obsolete.")]
|
||||
public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
|
||||
@@ -937,7 +928,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Guid info returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
|
||||
[HttpGet("GuideInfo")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<GuideInfo> GetGuideInfo()
|
||||
{
|
||||
@@ -951,7 +942,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Created tuner host returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
|
||||
[HttpPost("TunerHosts")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
|
||||
{
|
||||
@@ -965,7 +956,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Tuner host deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("TunerHosts")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DeleteTunerHost([FromQuery] string? id)
|
||||
{
|
||||
@@ -981,7 +972,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Default listings provider info returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
|
||||
[HttpGet("ListingProviders/Default")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
|
||||
{
|
||||
@@ -998,7 +989,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Created listings provider returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
|
||||
[HttpPost("ListingProviders")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
|
||||
@@ -1025,7 +1016,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Listing provider deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("ListingProviders")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DeleteListingProvider([FromQuery] string? id)
|
||||
{
|
||||
@@ -1043,7 +1034,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Available lineups returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
|
||||
[HttpGet("ListingProviders/Lineups")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
|
||||
[FromQuery] string? id,
|
||||
@@ -1060,7 +1051,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Available countries returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
|
||||
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile(MediaTypeNames.Application.Json)]
|
||||
public async Task<ActionResult> GetSchedulesDirectCountries()
|
||||
@@ -1081,7 +1072,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Channel mapping options returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
|
||||
[HttpGet("ChannelMappingOptions")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
|
||||
{
|
||||
@@ -1119,7 +1110,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Created channel mapping returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
|
||||
[HttpPost("ChannelMappings")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
|
||||
{
|
||||
@@ -1132,7 +1123,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Tuner host types returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
|
||||
[HttpGet("TunerHosts/Types")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
|
||||
{
|
||||
@@ -1147,7 +1138,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
|
||||
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
|
||||
[HttpGet("Tuners/Discover")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
|
||||
{
|
||||
@@ -1207,20 +1198,5 @@ namespace Jellyfin.Api.Controllers
|
||||
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
|
||||
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
|
||||
}
|
||||
|
||||
private async Task AssertUserCanManageLiveTv()
|
||||
{
|
||||
var user = await _sessionContext.GetUser(Request).ConfigureAwait(false);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new SecurityException("Anonymous live tv management is not allowed.");
|
||||
}
|
||||
|
||||
if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
|
||||
{
|
||||
throw new SecurityException("The current user does not have permission to manage live tv.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Package repositories saved.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Repositories")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos)
|
||||
{
|
||||
|
||||
@@ -98,7 +98,10 @@ namespace Jellyfin.Api.Controllers
|
||||
Limit = limit ?? 0
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>(peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray());
|
||||
return new QueryResult<BaseItemDto>(
|
||||
peopleItems
|
||||
.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -187,7 +188,7 @@ namespace Jellyfin.Api.Controllers
|
||||
result.AlbumArtist = album.AlbumArtist;
|
||||
break;
|
||||
case Audio song:
|
||||
result.AlbumArtist = song.AlbumArtists?[0];
|
||||
result.AlbumArtist = song.AlbumArtists?.FirstOrDefault();
|
||||
result.Artists = song.Artists;
|
||||
|
||||
MusicAlbum musicAlbum = song.AlbumEntity;
|
||||
|
||||
@@ -57,6 +57,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="isMovie">Optional filter for live tv movies.</param>
|
||||
/// <param name="isSeries">Optional filter for live tv series.</param>
|
||||
/// <param name="isNews">Optional filter for live tv news.</param>
|
||||
/// <param name="isKids">Optional filter for live tv kids.</param>
|
||||
/// <param name="isSports">Optional filter for live tv sports.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
@@ -140,6 +145,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSeries,
|
||||
[FromQuery] bool? isNews,
|
||||
[FromQuery] bool? isKids,
|
||||
[FromQuery] bool? isSports,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
@@ -224,6 +234,11 @@ namespace Jellyfin.Api.Controllers
|
||||
hasImdbId,
|
||||
hasTmdbId,
|
||||
hasTvdbId,
|
||||
isMovie,
|
||||
isSeries,
|
||||
isNews,
|
||||
isKids,
|
||||
isSports,
|
||||
excludeItemIds,
|
||||
startIndex,
|
||||
limit,
|
||||
|
||||
@@ -102,13 +102,13 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] int? transcodingAudioChannels,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] string? transcodingContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? transcodingContainer,
|
||||
[FromQuery] string? transcodingProtocol,
|
||||
[FromQuery] int? maxAudioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
@@ -117,7 +117,13 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool enableRedirection = true)
|
||||
{
|
||||
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
||||
(await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId;
|
||||
var authorizationInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
authorizationInfo.DeviceId = deviceId;
|
||||
|
||||
if (!userId.HasValue || userId.Value.Equals(Guid.Empty))
|
||||
{
|
||||
userId = authorizationInfo.UserId;
|
||||
}
|
||||
|
||||
var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -282,16 +282,19 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var success = await _userManager.AuthenticateUser(
|
||||
user.Username,
|
||||
request.CurrentPw,
|
||||
request.CurrentPw,
|
||||
HttpContext.GetNormalizedRemoteIp().ToString(),
|
||||
false).ConfigureAwait(false);
|
||||
|
||||
if (success == null)
|
||||
if (!HttpContext.User.IsInRole(UserRoles.Administrator))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
|
||||
var success = await _userManager.AuthenticateUser(
|
||||
user.Username,
|
||||
request.CurrentPw,
|
||||
request.CurrentPw,
|
||||
HttpContext.GetNormalizedRemoteIp().ToString(),
|
||||
false).ConfigureAwait(false);
|
||||
|
||||
if (success == null)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
|
||||
}
|
||||
}
|
||||
|
||||
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
|
||||
|
||||
@@ -318,18 +318,18 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetVideoStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -361,8 +361,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -427,7 +427,7 @@ namespace Jellyfin.Api.Controllers
|
||||
StreamOptions = streamOptions
|
||||
};
|
||||
|
||||
using var state = await StreamingHelpers.GetStreamingState(
|
||||
var state = await StreamingHelpers.GetStreamingState(
|
||||
streamingRequest,
|
||||
Request,
|
||||
_authContext,
|
||||
@@ -578,12 +578,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -615,8 +615,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
|
||||
@@ -8,6 +8,8 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -202,8 +204,18 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
if (state.VideoStream != null && state.VideoRequest != null)
|
||||
{
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
|
||||
// Provide SDR HEVC entrance for backward compatibility.
|
||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
if (encodingOptions.AllowHevcEncoding
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& !string.IsNullOrEmpty(state.VideoStream.VideoRange)
|
||||
&& string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -216,11 +228,26 @@ namespace Jellyfin.Api.Helpers
|
||||
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
|
||||
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
||||
|
||||
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0;
|
||||
var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
|
||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
|
||||
var sdrOutputAudioBitrate = 0;
|
||||
if (EncodingHelper.LosslessAudioCodecs.Contains(state.VideoRequest.AudioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sdrOutputAudioBitrate = state.AudioStream.BitRate ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream, state.OutputAudioChannels) ?? 0;
|
||||
}
|
||||
|
||||
AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||
var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
|
||||
// Restore the video codec
|
||||
state.OutputVideoCodec = "copy";
|
||||
@@ -250,6 +277,13 @@ namespace Jellyfin.Api.Helpers
|
||||
state.VideoStream.Level = originalLevel;
|
||||
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
||||
builder.Append(newPlaylist);
|
||||
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,6 +642,11 @@ namespace Jellyfin.Api.Helpers
|
||||
return HlsCodecStringHelpers.GetALACString();
|
||||
}
|
||||
|
||||
if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HlsCodecStringHelpers.GetOPUSString();
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
@@ -706,7 +745,19 @@ namespace Jellyfin.Api.Helpers
|
||||
return oldPlaylist.Replace(
|
||||
oldValue.ToString(),
|
||||
newValue.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
|
||||
{
|
||||
if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
|
||||
|
||||
return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,18 @@ namespace Jellyfin.Api.Helpers
|
||||
/// <summary>
|
||||
/// Codec name for FLAC.
|
||||
/// </summary>
|
||||
public const string FLAC = "fLaC";
|
||||
public const string FLAC = "flac";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for ALAC.
|
||||
/// </summary>
|
||||
public const string ALAC = "alac";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for OPUS.
|
||||
/// </summary>
|
||||
public const string OPUS = "opus";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a MP3 codec string.
|
||||
/// </summary>
|
||||
@@ -101,6 +106,15 @@ namespace Jellyfin.Api.Helpers
|
||||
return ALAC;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an OPUS codec string.
|
||||
/// </summary>
|
||||
/// <returns>OPUS codec string.</returns>
|
||||
public static string GetOPUSString()
|
||||
{
|
||||
return OPUS;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a H.264 codec string.
|
||||
/// </summary>
|
||||
|
||||
@@ -256,9 +256,17 @@ namespace Jellyfin.Api.Helpers
|
||||
streamInfo.StartPositionTicks = startTimeTicks;
|
||||
|
||||
mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
|
||||
|
||||
// Players do not handle this being set according to PlayMethod
|
||||
mediaSource.SupportsDirectStream = options.EnableDirectStream ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream : streamInfo.PlayMethod == PlayMethod.DirectPlay;
|
||||
mediaSource.SupportsTranscoding = streamInfo.PlayMethod == PlayMethod.DirectStream || mediaSource.TranscodingContainer != null;
|
||||
mediaSource.SupportsDirectStream =
|
||||
options.EnableDirectStream
|
||||
? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream
|
||||
: streamInfo.PlayMethod == PlayMethod.DirectPlay;
|
||||
|
||||
mediaSource.SupportsTranscoding =
|
||||
streamInfo.PlayMethod == PlayMethod.DirectStream
|
||||
|| mediaSource.TranscodingContainer != null
|
||||
|| profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
|
||||
|
||||
if (item is Audio)
|
||||
{
|
||||
@@ -290,7 +298,7 @@ namespace Jellyfin.Api.Helpers
|
||||
}
|
||||
else
|
||||
{
|
||||
if (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream)
|
||||
if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
|
||||
{
|
||||
streamInfo.PlayMethod = PlayMethod.Transcode;
|
||||
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
|
||||
|
||||
@@ -179,15 +179,21 @@ namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
containerInternal = streamingRequest.Static ?
|
||||
StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
|
||||
: GetOutputFileExtension(state);
|
||||
: GetOutputFileExtension(state, mediaSource);
|
||||
}
|
||||
|
||||
var outputAudioCodec = streamingRequest.AudioCodec;
|
||||
if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec))
|
||||
{
|
||||
state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
|
||||
}
|
||||
|
||||
state.OutputAudioCodec = outputAudioCodec;
|
||||
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
||||
|
||||
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
|
||||
|
||||
state.OutputAudioCodec = streamingRequest.AudioCodec;
|
||||
|
||||
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
|
||||
|
||||
if (state.VideoRequest != null)
|
||||
@@ -235,7 +241,7 @@ namespace Jellyfin.Api.Helpers
|
||||
ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
|
||||
|
||||
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
||||
? GetOutputFileExtension(state)
|
||||
? GetOutputFileExtension(state, mediaSource)
|
||||
: ("." + state.OutputContainer);
|
||||
|
||||
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||
@@ -312,7 +318,7 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
responseHeaders.Add(
|
||||
"contentFeatures.dlna.org",
|
||||
ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
|
||||
ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,8 +415,9 @@ namespace Jellyfin.Api.Helpers
|
||||
/// Gets the output file extension.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="mediaSource">The mediaSource.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string? GetOutputFileExtension(StreamState state)
|
||||
private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
|
||||
{
|
||||
var ext = Path.GetExtension(state.RequestedUrl);
|
||||
|
||||
@@ -425,7 +432,7 @@ namespace Jellyfin.Api.Helpers
|
||||
var videoCodec = state.Request.VideoCodec;
|
||||
|
||||
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
||||
string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".ts";
|
||||
}
|
||||
@@ -474,6 +481,13 @@ namespace Jellyfin.Api.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the container of mediaSource
|
||||
if (!string.IsNullOrEmpty(mediaSource?.Container))
|
||||
{
|
||||
var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase);
|
||||
return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -533,6 +547,7 @@ namespace Jellyfin.Api.Helpers
|
||||
state.TargetVideoBitDepth,
|
||||
state.OutputVideoBitrate,
|
||||
state.TargetVideoProfile,
|
||||
state.TargetVideoRangeType,
|
||||
state.TargetVideoLevel,
|
||||
state.TargetFramerate,
|
||||
state.TargetPacketLength,
|
||||
|
||||
@@ -654,8 +654,8 @@ namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
if (EnableThrottling(state))
|
||||
{
|
||||
transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem);
|
||||
state.TranscodingThrottler.Start();
|
||||
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
||||
transcodingJob.TranscodingThrottler.Start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,18 +663,11 @@ namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
|
||||
// enable throttling when NOT using hardware acceleration
|
||||
if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
|
||||
{
|
||||
return state.InputProtocol == MediaProtocol.File &&
|
||||
state.RunTimeTicks.HasValue &&
|
||||
state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
|
||||
state.IsInputVideo &&
|
||||
state.VideoType == VideoType.VideoFile &&
|
||||
!EncodingHelper.IsCopyCodec(state.OutputVideoCodec);
|
||||
}
|
||||
|
||||
return false;
|
||||
return state.InputProtocol == MediaProtocol.File &&
|
||||
state.RunTimeTicks.HasValue &&
|
||||
state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
|
||||
state.IsInputVideo &&
|
||||
state.VideoType == VideoType.VideoFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -17,15 +17,16 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
|
||||
<ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -17,6 +18,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
||||
private readonly ILogger<TranscodingThrottler> _logger;
|
||||
private readonly IConfigurationManager _config;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private Timer? _timer;
|
||||
private bool _isPaused;
|
||||
|
||||
@@ -27,12 +29,14 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param>
|
||||
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem)
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_job = job;
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_fileSystem = fileSystem;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,7 +59,8 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
||||
|
||||
try
|
||||
{
|
||||
await _job.Process!.StandardInput.WriteLineAsync().ConfigureAwait(false);
|
||||
var resumeKey = _mediaEncoder.IsPkeyPauseSupported ? "u" : Environment.NewLine;
|
||||
await _job.Process!.StandardInput.WriteAsync(resumeKey).ConfigureAwait(false);
|
||||
_isPaused = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -125,11 +130,13 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
||||
{
|
||||
if (!_isPaused)
|
||||
{
|
||||
_logger.LogDebug("Sending pause command to ffmpeg");
|
||||
var pauseKey = _mediaEncoder.IsPkeyPauseSupported ? "p" : "c";
|
||||
|
||||
_logger.LogDebug("Sending pause command [{Key}] to ffmpeg", pauseKey);
|
||||
|
||||
try
|
||||
{
|
||||
await _job.Process!.StandardInput.WriteAsync("c").ConfigureAwait(false);
|
||||
await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false);
|
||||
_isPaused = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -47,11 +47,6 @@ namespace Jellyfin.Api.Models.StreamingDtos
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transcoding throttler.
|
||||
/// </summary>
|
||||
public TranscodingThrottler? TranscodingThrottler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the video request.
|
||||
/// </summary>
|
||||
@@ -191,11 +186,8 @@ namespace Jellyfin.Api.Models.StreamingDtos
|
||||
{
|
||||
_mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
TranscodingThrottler?.Dispose();
|
||||
}
|
||||
|
||||
TranscodingThrottler = null;
|
||||
TranscodingJob = null;
|
||||
|
||||
_disposed = true;
|
||||
|
||||
@@ -362,7 +362,7 @@ namespace Jellyfin.Data.Entities
|
||||
/// <returns><c>True</c> if the user has the specified permission.</returns>
|
||||
public bool HasPermission(PermissionKind kind)
|
||||
{
|
||||
return Permissions.First(p => p.Kind == kind).Value;
|
||||
return Permissions.FirstOrDefault(p => p.Kind == kind)?.Value ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -372,7 +372,15 @@ namespace Jellyfin.Data.Entities
|
||||
/// <param name="value">The value to set.</param>
|
||||
public void SetPermission(PermissionKind kind, bool value)
|
||||
{
|
||||
Permissions.First(p => p.Kind == kind).Value = value;
|
||||
var currentPermission = Permissions.FirstOrDefault(p => p.Kind == kind);
|
||||
if (currentPermission == null)
|
||||
{
|
||||
Permissions.Add(new Permission(kind, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
currentPermission.Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -382,9 +390,9 @@ namespace Jellyfin.Data.Entities
|
||||
/// <returns>A string array containing the user's preferences.</returns>
|
||||
public string[] GetPreference(PreferenceKind preference)
|
||||
{
|
||||
var val = Preferences.First(p => p.Kind == preference).Value;
|
||||
var val = Preferences.FirstOrDefault(p => p.Kind == preference)?.Value;
|
||||
|
||||
return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter);
|
||||
return string.IsNullOrEmpty(val) ? Array.Empty<string>() : val.Split(Delimiter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -395,7 +403,7 @@ namespace Jellyfin.Data.Entities
|
||||
/// <returns>A {T} array containing the user's preference.</returns>
|
||||
public T[] GetPreferenceValues<T>(PreferenceKind preference)
|
||||
{
|
||||
var val = Preferences.First(p => p.Kind == preference).Value;
|
||||
var val = Preferences.FirstOrDefault(p => p.Kind == preference)?.Value;
|
||||
if (string.IsNullOrEmpty(val))
|
||||
{
|
||||
return Array.Empty<T>();
|
||||
@@ -432,8 +440,16 @@ namespace Jellyfin.Data.Entities
|
||||
/// <param name="values">The values.</param>
|
||||
public void SetPreference(PreferenceKind preference, string[] values)
|
||||
{
|
||||
Preferences.First(p => p.Kind == preference).Value
|
||||
= string.Join(Delimiter, values);
|
||||
var value = string.Join(Delimiter, values);
|
||||
var currentPreference = Preferences.FirstOrDefault(p => p.Kind == preference);
|
||||
if (currentPreference == null)
|
||||
{
|
||||
Preferences.Add(new Preference(preference, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
currentPreference.Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -444,8 +460,16 @@ namespace Jellyfin.Data.Entities
|
||||
/// <typeparam name="T">The type of value.</typeparam>
|
||||
public void SetPreference<T>(PreferenceKind preference, T[] values)
|
||||
{
|
||||
Preferences.First(p => p.Kind == preference).Value
|
||||
= string.Join(Delimiter, values);
|
||||
var value = string.Join(Delimiter, values);
|
||||
var currentPreference = Preferences.FirstOrDefault(p => p.Kind == preference);
|
||||
if (currentPreference == null)
|
||||
{
|
||||
Preferences.Add(new Preference(preference, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
currentPreference.Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.8.0</VersionPrefix>
|
||||
<VersionPrefix>10.8.13</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BlurHashSharp" Version="1.2.0" />
|
||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.3" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.2" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.2" />
|
||||
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,14 +3,14 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using BlurHashSharp.SkiaSharp;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SkiaSharp;
|
||||
using static Jellyfin.Drawing.Skia.SkiaHelper;
|
||||
using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia
|
||||
{
|
||||
@@ -19,8 +19,7 @@ namespace Jellyfin.Drawing.Skia
|
||||
/// </summary>
|
||||
public class SkiaEncoder : IImageEncoder
|
||||
{
|
||||
private static readonly HashSet<string> _transparentImageTypes
|
||||
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
||||
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
||||
|
||||
private readonly ILogger<SkiaEncoder> _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
@@ -71,7 +70,7 @@ namespace Jellyfin.Drawing.Skia
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
|
||||
=> new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
|
||||
=> new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
|
||||
|
||||
/// <summary>
|
||||
/// Check if the native lib is available.
|
||||
@@ -109,9 +108,7 @@ namespace Jellyfin.Drawing.Skia
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <exception cref="ArgumentNullException">The path is null.</exception>
|
||||
/// <exception cref="FileNotFoundException">The path is not valid.</exception>
|
||||
/// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
|
||||
public ImageDimensions GetImageSize(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
@@ -119,12 +116,27 @@ namespace Jellyfin.Drawing.Skia
|
||||
throw new FileNotFoundException("File not found", path);
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var svg = new SKSvg();
|
||||
svg.Load(path);
|
||||
return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height));
|
||||
}
|
||||
|
||||
using var codec = SKCodec.Create(path, out SKCodecResult result);
|
||||
EnsureSuccess(result);
|
||||
|
||||
var info = codec.Info;
|
||||
|
||||
return new ImageDimensions(info.Width, info.Height);
|
||||
switch (result)
|
||||
{
|
||||
case SKCodecResult.Success:
|
||||
var info = codec.Info;
|
||||
return new ImageDimensions(info.Width, info.Height);
|
||||
case SKCodecResult.Unimplemented:
|
||||
_logger.LogDebug("Image format not supported: {FilePath}", path);
|
||||
return new ImageDimensions(0, 0);
|
||||
default:
|
||||
_logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
|
||||
return new ImageDimensions(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -138,6 +150,13 @@ namespace Jellyfin.Drawing.Skia
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
|
||||
if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Any larger than 128x128 is too slow and there's no visually discernible difference
|
||||
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
|
||||
}
|
||||
@@ -378,6 +397,13 @@ namespace Jellyfin.Drawing.Skia
|
||||
throw new ArgumentException("String can't be empty.", nameof(outputPath));
|
||||
}
|
||||
|
||||
var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
|
||||
if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
|
||||
return inputPath;
|
||||
}
|
||||
|
||||
var skiaOutputFormat = GetImageFormat(outputFormat);
|
||||
|
||||
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
|
||||
|
||||
@@ -8,19 +8,6 @@ namespace Jellyfin.Drawing.Skia
|
||||
/// </summary>
|
||||
public static class SkiaHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures the result is a success
|
||||
/// by throwing an exception when that's not the case.
|
||||
/// </summary>
|
||||
/// <param name="result">The result returned by Skia.</param>
|
||||
public static void EnsureSuccess(SKCodecResult result)
|
||||
{
|
||||
if (result != SKCodecResult.Success)
|
||||
{
|
||||
throw new SkiaCodecException(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next valid image as a bitmap.
|
||||
/// </summary>
|
||||
|
||||
@@ -463,6 +463,18 @@ namespace Jellyfin.Networking.Manager
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInLocalNetwork(IPObject address)
|
||||
{
|
||||
return IsInLocalNetwork(address.Address);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInLocalNetwork(string address)
|
||||
{
|
||||
return IPHost.TryParse(address, out IPHost ipHost) && IsInLocalNetwork(ipHost);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInLocalNetwork(IPAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
@@ -481,36 +493,7 @@ namespace Jellyfin.Networking.Manager
|
||||
}
|
||||
|
||||
// As private addresses can be redefined by Configuration.LocalNetworkAddresses
|
||||
return address.IsLoopback() || (_lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInLocalNetwork(string address)
|
||||
{
|
||||
if (IPHost.TryParse(address, out IPHost ep))
|
||||
{
|
||||
return _lanSubnets.ContainsAddress(ep) && !_excludedSubnets.ContainsAddress(ep);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInLocalNetwork(IPAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
// See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
|
||||
if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// As private addresses can be redefined by Configuration.LocalNetworkAddresses
|
||||
return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address);
|
||||
return IPAddress.IsLoopback(address) || (_lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
657
Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
generated
Normal file
657
Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
generated
Normal file
@@ -0,0 +1,657 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
[DbContext(typeof(JellyfinDb))]
|
||||
[Migration("20221022080052_AddIndexActivityLogsDateCreated")]
|
||||
partial class AddIndexActivityLogsDateCreated
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("jellyfin")
|
||||
.HasAnnotation("ProductVersion", "6.0.9");
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DayOfWeek")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("EndHour")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("StartHour")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AccessSchedules", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LogSeverity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ShortOverview")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DateCreated");
|
||||
|
||||
b.ToTable("ActivityLogs", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CustomItemDisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChromecastVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DashboardTheme")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EnableNextVideoInfoOverlay")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("IndexBy")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ScrollDirection")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ShowBackdrop")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ShowSidebar")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SkipBackwardLength")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SkipForwardLength")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TvHome")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "ItemId", "Client")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DisplayPreferencesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DisplayPreferencesId");
|
||||
|
||||
b.ToTable("HomeSection", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ImageInfos", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("IndexBy")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("RememberIndexing")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberSorting")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SortBy")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ViewType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ItemDisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("Permission_Permissions_Guid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Value")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "Kind")
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Permissions", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("Preference_Preferences_Guid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "Kind")
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Preferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateLastActivity")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccessToken")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ApiKeys", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AppName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AppVersion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateLastActivity")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.HasIndex("AccessToken", "DateLastActivity");
|
||||
|
||||
b.HasIndex("DeviceId", "DateLastActivity");
|
||||
|
||||
b.HasIndex("UserId", "DeviceId");
|
||||
|
||||
b.ToTable("Devices", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CustomName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DeviceOptions", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AudioLanguagePreference")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AuthenticationProviderId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DisplayCollectionsView")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("DisplayMissingEpisodes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("EasyPassword")
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EnableAutoLogin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableLocalPassword")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableNextEpisodeAutoPlay")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableUserPreferenceAccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("HidePlayedInLatest")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("InternalId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("InvalidLoginAttemptCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastActivityDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastLoginDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("LoginAttemptsBeforeLockout")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxActiveSessions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MaxParentalAgeRating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("MustUpdatePassword")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordResetProviderId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PlayDefaultAudioTrack")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberAudioSelections")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberSubtitleSelections")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("RemoteClientBitrateLimit")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SubtitleLanguagePreference")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SubtitleMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SyncPlayAccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.UseCollation("NOCASE");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("AccessSchedules")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("DisplayPreferences")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
|
||||
.WithMany("HomeSections")
|
||||
.HasForeignKey("DisplayPreferencesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithOne("ProfileImage")
|
||||
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("ItemDisplayPreferences")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("Permissions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("Preferences")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
{
|
||||
b.Navigation("HomeSections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("AccessSchedules");
|
||||
|
||||
b.Navigation("DisplayPreferences");
|
||||
|
||||
b.Navigation("ItemDisplayPreferences");
|
||||
|
||||
b.Navigation("Permissions");
|
||||
|
||||
b.Navigation("Preferences");
|
||||
|
||||
b.Navigation("ProfileImage");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma warning disable CS1591, SA1601
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
public partial class AddIndexActivityLogsDateCreated : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ActivityLogs_DateCreated",
|
||||
schema: "jellyfin",
|
||||
table: "ActivityLogs",
|
||||
column: "DateCreated");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ActivityLogs_DateCreated",
|
||||
schema: "jellyfin",
|
||||
table: "ActivityLogs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
[DbContext(typeof(JellyfinDb))]
|
||||
@@ -15,7 +17,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("jellyfin")
|
||||
.HasAnnotation("ProductVersion", "5.0.7");
|
||||
.HasAnnotation("ProductVersion", "6.0.9");
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
{
|
||||
@@ -39,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AccessSchedules");
|
||||
b.ToTable("AccessSchedules", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
|
||||
@@ -85,7 +87,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ActivityLogs");
|
||||
b.HasIndex("DateCreated");
|
||||
|
||||
b.ToTable("ActivityLogs", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
|
||||
@@ -117,7 +121,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CustomItemDisplayPreferences");
|
||||
b.ToTable("CustomItemDisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
@@ -174,7 +178,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("UserId", "ItemId", "Client")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DisplayPreferences");
|
||||
b.ToTable("DisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||
@@ -196,7 +200,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasIndex("DisplayPreferencesId");
|
||||
|
||||
b.ToTable("HomeSection");
|
||||
b.ToTable("HomeSection", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||
@@ -221,7 +225,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ImageInfos");
|
||||
b.ToTable("ImageInfos", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||
@@ -265,7 +269,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ItemDisplayPreferences");
|
||||
b.ToTable("ItemDisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||
@@ -296,7 +300,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Permissions");
|
||||
b.ToTable("Permissions", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||
@@ -329,7 +333,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Preferences");
|
||||
b.ToTable("Preferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
|
||||
@@ -358,7 +362,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("AccessToken")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
b.ToTable("ApiKeys", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
||||
@@ -416,7 +420,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasIndex("UserId", "DeviceId");
|
||||
|
||||
b.ToTable("Devices");
|
||||
b.ToTable("Devices", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
|
||||
@@ -437,7 +441,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("DeviceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DeviceOptions");
|
||||
b.ToTable("DeviceOptions", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||
@@ -550,7 +554,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
b.ToTable("Users", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// FluentAPI configuration for the ActivityLog entity.
|
||||
/// </summary>
|
||||
public class ActivityLogConfiguration : IEntityTypeConfiguration<ActivityLog>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<ActivityLog> builder)
|
||||
{
|
||||
builder.HasIndex(entity => entity.DateCreated);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -16,11 +17,16 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
{
|
||||
private readonly JellyfinDbProvider _jellyfinDbProvider;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IServerApplicationHost _serverApplicationHost;
|
||||
|
||||
public AuthorizationContext(JellyfinDbProvider jellyfinDb, IUserManager userManager)
|
||||
public AuthorizationContext(
|
||||
JellyfinDbProvider jellyfinDb,
|
||||
IUserManager userManager,
|
||||
IServerApplicationHost serverApplicationHost)
|
||||
{
|
||||
_jellyfinDbProvider = jellyfinDb;
|
||||
_userManager = userManager;
|
||||
_serverApplicationHost = serverApplicationHost;
|
||||
}
|
||||
|
||||
public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
|
||||
@@ -187,17 +193,17 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
authInfo.Token = key.AccessToken;
|
||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||
{
|
||||
authInfo.DeviceId = string.Empty;
|
||||
authInfo.DeviceId = _serverApplicationHost.SystemId;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||
{
|
||||
authInfo.Device = string.Empty;
|
||||
authInfo.Device = _serverApplicationHost.Name;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||
{
|
||||
authInfo.Version = string.Empty;
|
||||
authInfo.Version = _serverApplicationHost.ApplicationVersionString;
|
||||
}
|
||||
|
||||
authInfo.IsApiKey = true;
|
||||
|
||||
@@ -14,6 +14,8 @@ using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy;
|
||||
using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy;
|
||||
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
||||
using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
|
||||
using Jellyfin.Api.Auth.LiveTvAccessPolicy;
|
||||
using Jellyfin.Api.Auth.LiveTvManagementPolicy;
|
||||
using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
|
||||
using Jellyfin.Api.Auth.LocalAccessPolicy;
|
||||
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
||||
@@ -66,6 +68,8 @@ namespace Jellyfin.Server.Extensions
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, LiveTvAccessHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, LiveTvManagementHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
|
||||
return serviceCollection.AddAuthorizationCore(options =>
|
||||
{
|
||||
@@ -167,6 +171,20 @@ namespace Jellyfin.Server.Extensions
|
||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||
policy.AddRequirements(new AnonymousLanAccessRequirement());
|
||||
});
|
||||
options.AddPolicy(
|
||||
Policies.LiveTvAccess,
|
||||
policy =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||
policy.AddRequirements(new LiveTvAccessRequirement());
|
||||
});
|
||||
options.AddPolicy(
|
||||
Policies.LiveTvManagement,
|
||||
policy =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||
policy.AddRequirements(new LiveTvManagementRequirement());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -434,12 +452,22 @@ namespace Jellyfin.Server.Extensions
|
||||
options.MapType<TranscodeReason>(() =>
|
||||
new OpenApiSchema
|
||||
{
|
||||
Type = "string",
|
||||
Enum = Enum.GetNames<TranscodeReason>()
|
||||
.Select(e => new OpenApiString(e))
|
||||
.Cast<IOpenApiAny>()
|
||||
.ToArray()
|
||||
Type = "array",
|
||||
Items = new OpenApiSchema
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Id = nameof(TranscodeReason),
|
||||
Type = ReferenceType.Schema,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
|
||||
options.MapType<Version>(() => new OpenApiSchema
|
||||
{
|
||||
Type = "string"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Server.Migrations;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.ApiClient;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Session;
|
||||
using MediaBrowser.Model.SyncPlay;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
@@ -14,6 +20,19 @@ namespace Jellyfin.Server.Filters
|
||||
/// </summary>
|
||||
public class AdditionalModelFilter : IDocumentFilter
|
||||
{
|
||||
// Array of options that should not be visible in the api spec.
|
||||
private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions) };
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AdditionalModelFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public AdditionalModelFilter(IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
@@ -29,6 +48,25 @@ namespace Jellyfin.Server.Filters
|
||||
|
||||
context.SchemaGenerator.GenerateSchema(typeof(SessionMessageType), context.SchemaRepository);
|
||||
context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository);
|
||||
|
||||
foreach (var configuration in _serverConfigurationManager.GetConfigurationStores())
|
||||
{
|
||||
if (_ignoredConfigurations.IndexOf(configuration.ConfigurationType) != -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository);
|
||||
}
|
||||
|
||||
context.SchemaRepository.AddDefinition(nameof(TranscodeReason), new OpenApiSchema
|
||||
{
|
||||
Type = "string",
|
||||
Enum = Enum.GetNames<TranscodeReason>()
|
||||
.Select(e => new OpenApiString(e))
|
||||
.Cast<IOpenApiAny>()
|
||||
.ToArray()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.9" />
|
||||
<PackageReference Include="prometheus-net" Version="6.0.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
|
||||
@@ -48,7 +48,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.3.0" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.7" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -19,41 +19,44 @@ namespace Jellyfin.Server.Middleware
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ResponseTimeMiddleware> _logger;
|
||||
|
||||
private readonly bool _enableWarning;
|
||||
private readonly long _warningThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">Next request delegate.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public ResponseTimeMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<ResponseTimeMiddleware> logger,
|
||||
IServerConfigurationManager serverConfigurationManager)
|
||||
ILogger<ResponseTimeMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
|
||||
_enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
|
||||
_warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke request.
|
||||
/// </summary>
|
||||
/// <param name="context">Request context.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Invoke(HttpContext context)
|
||||
public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
|
||||
var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
|
||||
var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
|
||||
context.Response.OnStarting(() =>
|
||||
{
|
||||
watch.Stop();
|
||||
LogWarning(context, watch);
|
||||
if (enableWarning && watch.ElapsedMilliseconds > warningThreshold)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
|
||||
context.Request.GetDisplayUrl(),
|
||||
context.GetNormalizedRemoteIp(),
|
||||
watch.Elapsed,
|
||||
context.Response.StatusCode);
|
||||
}
|
||||
|
||||
var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
|
||||
context.Response.Headers[ResponseHeaderResponseTime] = responseTimeForCompleteRequest.ToString(CultureInfo.InvariantCulture);
|
||||
return Task.CompletedTask;
|
||||
@@ -62,18 +65,5 @@ namespace Jellyfin.Server.Middleware
|
||||
// Call the next delegate/middleware in the pipeline
|
||||
await this._next(context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void LogWarning(HttpContext context, Stopwatch watch)
|
||||
{
|
||||
if (_enableWarning && watch.ElapsedMilliseconds > _warningThreshold)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
|
||||
context.Request.GetDisplayUrl(),
|
||||
context.GetNormalizedRemoteIp(),
|
||||
watch.Elapsed,
|
||||
context.Response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Emby.Server.Implementations.Data;
|
||||
using Jellyfin.Data.Entities.Security;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
@@ -20,6 +21,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
private readonly ILogger<MigrateAuthenticationDb> _logger;
|
||||
private readonly JellyfinDbProvider _dbProvider;
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MigrateAuthenticationDb"/> class.
|
||||
@@ -27,11 +29,17 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="dbProvider">The database provider.</param>
|
||||
/// <param name="appPaths">The server application paths.</param>
|
||||
public MigrateAuthenticationDb(ILogger<MigrateAuthenticationDb> logger, JellyfinDbProvider dbProvider, IServerApplicationPaths appPaths)
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
public MigrateAuthenticationDb(
|
||||
ILogger<MigrateAuthenticationDb> logger,
|
||||
JellyfinDbProvider dbProvider,
|
||||
IServerApplicationPaths appPaths,
|
||||
IUserManager userManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbProvider = dbProvider;
|
||||
_appPaths = appPaths;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -74,6 +82,14 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
}
|
||||
else
|
||||
{
|
||||
var userId = new Guid(row[6].ToString());
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user is null)
|
||||
{
|
||||
// User doesn't exist, don't bring over the device.
|
||||
continue;
|
||||
}
|
||||
|
||||
dbContext.Devices.Add(new Device(
|
||||
new Guid(row[6].ToString()),
|
||||
row[3].ToString(),
|
||||
|
||||
@@ -243,7 +243,7 @@ namespace Jellyfin.Server
|
||||
}
|
||||
}
|
||||
|
||||
appHost.Dispose();
|
||||
await appHost.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_restartOnShutdown)
|
||||
@@ -545,12 +545,14 @@ namespace Jellyfin.Server
|
||||
const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
|
||||
Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
|
||||
?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
|
||||
Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
await using (resource.ConfigureAwait(false))
|
||||
await using (dst.ConfigureAwait(false))
|
||||
{
|
||||
// Copy the resource contents to the expected file path for the config file
|
||||
await resource.CopyToAsync(dst).ConfigureAwait(false);
|
||||
Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
await using (dst.ConfigureAwait(false))
|
||||
{
|
||||
// Copy the resource contents to the expected file path for the config file
|
||||
await resource.CopyToAsync(dst).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,12 @@ namespace MediaBrowser.Common.Configuration
|
||||
/// <returns>System.Object.</returns>
|
||||
object GetConfiguration(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the array of coniguration stores.
|
||||
/// </summary>
|
||||
/// <returns>Array of ConfigurationStore.</returns>
|
||||
ConfigurationStore[] GetConfigurationStores();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the configuration.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.8.0</VersionPrefix>
|
||||
<VersionPrefix>10.8.13</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -170,6 +170,11 @@ namespace MediaBrowser.Common.Net
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
if (address.AddressFamily != AddressFamily)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var (altAddress, altPrefix) = NetworkAddressOf(address, PrefixLength);
|
||||
return NetworkAddress.Address.Equals(altAddress) && NetworkAddress.PrefixLength >= altPrefix;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace MediaBrowser.Common.Net
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
if (IsLoopback(address))
|
||||
if (IPAddress.IsLoopback(address))
|
||||
{
|
||||
return (address, prefixLength);
|
||||
}
|
||||
@@ -102,31 +102,6 @@ namespace MediaBrowser.Common.Net
|
||||
return (new IPAddress(addressBytes), prefixLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests to see if the ip address is a Loopback address.
|
||||
/// </summary>
|
||||
/// <param name="address">Value to test.</param>
|
||||
/// <returns>True if it is.</returns>
|
||||
public static bool IsLoopback(IPAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
if (!address.Equals(IPAddress.None))
|
||||
{
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
return address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests to see if the ip address is an IP6 address.
|
||||
/// </summary>
|
||||
@@ -295,7 +270,7 @@ namespace MediaBrowser.Common.Net
|
||||
/// <returns>True if it is.</returns>
|
||||
public virtual bool IsLoopback()
|
||||
{
|
||||
return IsLoopback(Address);
|
||||
return IPAddress.IsLoopback(Address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,6 +23,11 @@ namespace MediaBrowser.Controller.ClientEvent
|
||||
{
|
||||
var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log";
|
||||
var logFilePath = Path.Combine(_applicationPaths.LogDirectoryPath, fileName);
|
||||
if (!Path.GetFullPath(logFilePath).StartsWith(_applicationPaths.LogDirectoryPath, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("Path resolved to filename not in log directory");
|
||||
}
|
||||
|
||||
await using var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
||||
await fileContents.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
return fileName;
|
||||
|
||||
@@ -50,6 +50,14 @@ namespace MediaBrowser.Controller.Drawing
|
||||
/// <returns>BlurHash.</returns>
|
||||
string GetImageBlurHash(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the blurhash of the image.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the image file.</param>
|
||||
/// <param name="imageDimensions">The image dimensions.</param>
|
||||
/// <returns>BlurHash.</returns>
|
||||
string GetImageBlurHash(string path, ImageDimensions imageDimensions);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache tag.
|
||||
/// </summary>
|
||||
|
||||
@@ -169,8 +169,8 @@ namespace MediaBrowser.Controller.Entities.Audio
|
||||
|
||||
var childUpdateType = ItemUpdateType.None;
|
||||
|
||||
// Refresh songs
|
||||
foreach (var item in items)
|
||||
// Refresh songs only and not m3u files in album folder
|
||||
foreach (var item in items.OfType<Audio>())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
@@ -48,7 +47,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
/// The supported image extensions.
|
||||
/// </summary>
|
||||
public static readonly string[] SupportedImageExtensions
|
||||
= new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" };
|
||||
= new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif" };
|
||||
|
||||
private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
|
||||
{
|
||||
@@ -1864,7 +1863,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
data.PlaybackPositionTicks = 0;
|
||||
}
|
||||
|
||||
data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
|
||||
data.LastPlayedDate = datePlayed ?? DateTime.UtcNow;
|
||||
data.Played = true;
|
||||
|
||||
UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
|
||||
|
||||
@@ -892,29 +892,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
|
||||
{
|
||||
var ids = query.ItemIds;
|
||||
int size = items.Count;
|
||||
|
||||
// ids can potentially contain non-unique guids, but query result cannot,
|
||||
// so we include only first occurrence of each guid
|
||||
var positions = new Dictionary<Guid, int>(size);
|
||||
int index = 0;
|
||||
for (int i = 0; i < ids.Length; i++)
|
||||
{
|
||||
if (positions.TryAdd(ids[i], index))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
var newItems = new BaseItem[size];
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
newItems[positions[item.Id]] = item;
|
||||
}
|
||||
|
||||
return newItems;
|
||||
return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
|
||||
}
|
||||
|
||||
public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user