mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-12 12:03:02 +03:00
Compare commits
458 Commits
v10.11.0-r
...
v10.8.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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)
|
- [cvium](https://github.com/cvium)
|
||||||
- [dannymichel](https://github.com/dannymichel)
|
- [dannymichel](https://github.com/dannymichel)
|
||||||
- [DaveChild](https://github.com/DaveChild)
|
- [DaveChild](https://github.com/DaveChild)
|
||||||
|
- [DavidFair](https://github.com/DavidFair)
|
||||||
- [Delgan](https://github.com/Delgan)
|
- [Delgan](https://github.com/Delgan)
|
||||||
- [dcrdev](https://github.com/dcrdev)
|
- [dcrdev](https://github.com/dcrdev)
|
||||||
- [dhartung](https://github.com/dhartung)
|
- [dhartung](https://github.com/dhartung)
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
- [dmitrylyzo](https://github.com/dmitrylyzo)
|
- [dmitrylyzo](https://github.com/dmitrylyzo)
|
||||||
- [DMouse10462](https://github.com/DMouse10462)
|
- [DMouse10462](https://github.com/DMouse10462)
|
||||||
- [DrPandemic](https://github.com/DrPandemic)
|
- [DrPandemic](https://github.com/DrPandemic)
|
||||||
|
- [eglia](https://github.com/eglia)
|
||||||
- [EraYaN](https://github.com/EraYaN)
|
- [EraYaN](https://github.com/EraYaN)
|
||||||
- [escabe](https://github.com/escabe)
|
- [escabe](https://github.com/escabe)
|
||||||
- [excelite](https://github.com/excelite)
|
- [excelite](https://github.com/excelite)
|
||||||
@@ -157,6 +159,8 @@
|
|||||||
- [jonas-resch](https://github.com/jonas-resch)
|
- [jonas-resch](https://github.com/jonas-resch)
|
||||||
- [vgambier](https://github.com/vgambier)
|
- [vgambier](https://github.com/vgambier)
|
||||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||||
|
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
||||||
|
- [TheTyrius](https://github.com/TheTyrius)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ COPY . .
|
|||||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
# because of changes in docker and systemd we need to not build in parallel at the moment
|
# 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
|
# 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
|
FROM app
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
|||||||
# Discard objs - may cause failures if exists
|
# Discard objs - may cause failures if exists
|
||||||
RUN find . -type d -name obj | xargs -r rm -r
|
RUN find . -type d -name obj | xargs -r rm -r
|
||||||
# Build
|
# 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
|
FROM app
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
|||||||
# Discard objs - may cause failures if exists
|
# Discard objs - may cause failures if exists
|
||||||
RUN find . -type d -name obj | xargs -r rm -r
|
RUN find . -type d -name obj | xargs -r rm -r
|
||||||
# Build
|
# 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
|
FROM app
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Configuration
|
|||||||
public DlnaOptions()
|
public DlnaOptions()
|
||||||
{
|
{
|
||||||
EnablePlayTo = true;
|
EnablePlayTo = true;
|
||||||
EnableServer = true;
|
EnableServer = false;
|
||||||
BlastAliveMessages = true;
|
BlastAliveMessages = true;
|
||||||
SendOnlyMatchedHost = true;
|
SendOnlyMatchedHost = true;
|
||||||
ClientDiscoveryIntervalSeconds = 60;
|
ClientDiscoveryIntervalSeconds = 60;
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ namespace Emby.Dlna.Didl
|
|||||||
streamInfo.IsDirectStream,
|
streamInfo.IsDirectStream,
|
||||||
streamInfo.RunTimeTicks ?? 0,
|
streamInfo.RunTimeTicks ?? 0,
|
||||||
streamInfo.TargetVideoProfile,
|
streamInfo.TargetVideoProfile,
|
||||||
|
streamInfo.TargetVideoRangeType,
|
||||||
streamInfo.TargetVideoLevel,
|
streamInfo.TargetVideoLevel,
|
||||||
streamInfo.TargetFramerate ?? 0,
|
streamInfo.TargetFramerate ?? 0,
|
||||||
streamInfo.TargetPacketLength,
|
streamInfo.TargetPacketLength,
|
||||||
@@ -376,6 +377,7 @@ namespace Emby.Dlna.Didl
|
|||||||
targetHeight,
|
targetHeight,
|
||||||
streamInfo.TargetVideoBitDepth,
|
streamInfo.TargetVideoBitDepth,
|
||||||
streamInfo.TargetVideoProfile,
|
streamInfo.TargetVideoProfile,
|
||||||
|
streamInfo.TargetVideoRangeType,
|
||||||
streamInfo.TargetVideoLevel,
|
streamInfo.TargetVideoLevel,
|
||||||
streamInfo.TargetFramerate ?? 0,
|
streamInfo.TargetFramerate ?? 0,
|
||||||
streamInfo.TargetPacketLength,
|
streamInfo.TargetPacketLength,
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ namespace Emby.Dlna.Main
|
|||||||
|
|
||||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
|
_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
|
var device = new SsdpRootDevice
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -561,6 +561,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
streamInfo.IsDirectStream,
|
streamInfo.IsDirectStream,
|
||||||
streamInfo.RunTimeTicks ?? 0,
|
streamInfo.RunTimeTicks ?? 0,
|
||||||
streamInfo.TargetVideoProfile,
|
streamInfo.TargetVideoProfile,
|
||||||
|
streamInfo.TargetVideoRangeType,
|
||||||
streamInfo.TargetVideoLevel,
|
streamInfo.TargetVideoLevel,
|
||||||
streamInfo.TargetFramerate ?? 0,
|
streamInfo.TargetFramerate ?? 0,
|
||||||
streamInfo.TargetPacketLength,
|
streamInfo.TargetPacketLength,
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ using System.IO;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Diacritics.Extensions;
|
|
||||||
using Emby.Dlna.Didl;
|
using Emby.Dlna.Didl;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|||||||
@@ -395,7 +395,13 @@ namespace Emby.Drawing
|
|||||||
public string GetImageBlurHash(string path)
|
public string GetImageBlurHash(string path)
|
||||||
{
|
{
|
||||||
var size = GetImageDimensions(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;
|
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.
|
// 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.
|
// 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
|
// 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 xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height);
|
||||||
float yCompF = xCompF * size.Height / size.Width;
|
float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width;
|
||||||
|
|
||||||
int xComp = Math.Min((int)xCompF + 1, 9);
|
int xComp = Math.Min((int)xCompF + 1, 9);
|
||||||
int yComp = Math.Min((int)yCompF + 1, 9);
|
int yComp = Math.Min((int)yCompF + 1, 9);
|
||||||
@@ -439,47 +445,46 @@ namespace Emby.Drawing
|
|||||||
.ToString("N", CultureInfo.InvariantCulture);
|
.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)
|
var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString();
|
||||||
.TrimStart('.')
|
|
||||||
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// These are just jpg files renamed as tbn
|
// These are just jpg files renamed as tbn
|
||||||
if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return (originalImagePath, dateModified);
|
return Task.FromResult((originalImagePath, dateModified));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
// TODO _mediaEncoder.ConvertImage is not implemented
|
||||||
{
|
// if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
||||||
try
|
// {
|
||||||
{
|
// try
|
||||||
string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
// {
|
||||||
|
// 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";
|
return Task.FromResult((originalImagePath, dateModified));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ namespace Emby.Naming.Common
|
|||||||
".mkv",
|
".mkv",
|
||||||
".mk3d",
|
".mk3d",
|
||||||
".mov",
|
".mov",
|
||||||
".mp2",
|
|
||||||
".mp4",
|
".mp4",
|
||||||
".mpe",
|
".mpe",
|
||||||
".mpeg",
|
".mpeg",
|
||||||
@@ -315,7 +314,7 @@ namespace Emby.Naming.Common
|
|||||||
// This isn't a Kodi naming rule, but the expression below causes false positives,
|
// This isn't a Kodi naming rule, but the expression below causes false positives,
|
||||||
// so we make sure this one gets tested first.
|
// so we make sure this one gets tested first.
|
||||||
// "Foo Bar 889"
|
// "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
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.8.0</VersionPrefix>
|
<VersionPrefix>10.8.10</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="TagLibSharp" Version="2.2.0" />
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -398,6 +398,12 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ConfigurationStore[] GetConfigurationStores()
|
||||||
|
{
|
||||||
|
return _configurationStores;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Type GetConfigurationType(string key)
|
public Type GetConfigurationType(string key)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ namespace Emby.Server.Implementations
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class CompositionRoot.
|
/// Class CompositionRoot.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class ApplicationHost : IServerApplicationHost, IDisposable
|
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The environment variable prefixes to log at server startup.
|
/// 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);
|
return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Published server ends with a /
|
return GetSmartApiUrl(request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -1114,13 +1106,13 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <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
|
// 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;
|
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
||||||
int? port = !allowHttps ? HttpPort : null;
|
int? port = !allowHttps ? HttpPort : null;
|
||||||
return GetLocalApiUrl(smart.Trim('/'), scheme, port);
|
return GetLocalApiUrl(smart, scheme, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <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
|
// 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.
|
// 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
|
return new UriBuilder
|
||||||
{
|
{
|
||||||
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
Scheme = scheme,
|
||||||
Host = hostname,
|
Host = hostname,
|
||||||
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
Port = port ?? (isHttps ? HttpsPort : HttpPort),
|
||||||
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||||
}.ToString().TrimEnd('/');
|
}.ToString().TrimEnd('/');
|
||||||
}
|
}
|
||||||
@@ -1230,5 +1224,49 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
_disposed = true;
|
_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;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Diacritics.Extensions;
|
|
||||||
using Emby.Server.Implementations.Playlists;
|
using Emby.Server.Implementations.Playlists;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
@@ -171,7 +170,15 @@ namespace Emby.Server.Implementations.Data
|
|||||||
"CodecTimeBase",
|
"CodecTimeBase",
|
||||||
"ColorPrimaries",
|
"ColorPrimaries",
|
||||||
"ColorSpace",
|
"ColorSpace",
|
||||||
"ColorTransfer"
|
"ColorTransfer",
|
||||||
|
"DvVersionMajor",
|
||||||
|
"DvVersionMinor",
|
||||||
|
"DvProfile",
|
||||||
|
"DvLevel",
|
||||||
|
"RpuPresentFlag",
|
||||||
|
"ElPresentFlag",
|
||||||
|
"BlPresentFlag",
|
||||||
|
"DvBlSignalCompatibilityId"
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly string _mediaStreamSaveColumnsInsertQuery =
|
private static readonly string _mediaStreamSaveColumnsInsertQuery =
|
||||||
@@ -342,7 +349,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
|
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
|
||||||
{
|
{
|
||||||
const string CreateMediaStreamsTableCommand
|
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
|
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))";
|
= "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", "ColorPrimaries", "TEXT", existingColumnNames);
|
||||||
AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
|
AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
|
||||||
AddColumn(db, "MediaStreams", "ColorTransfer", "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);
|
TransactionMode);
|
||||||
|
|
||||||
@@ -2404,7 +2420,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
|
|
||||||
// genres, tags, studios, person, year?
|
// 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)
|
if (item is MusicArtist)
|
||||||
{
|
{
|
||||||
@@ -3059,12 +3075,12 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase))
|
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))
|
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))
|
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))
|
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))
|
if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -3147,6 +3163,11 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return ItemSortBy.IndexNumber;
|
return ItemSortBy.IndexNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(name, ItemSortBy.SimilarityScore, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return ItemSortBy.SimilarityScore;
|
||||||
|
}
|
||||||
|
|
||||||
// Unknown SortBy, just sort by the SortName.
|
// Unknown SortBy, just sort by the SortName.
|
||||||
return ItemSortBy.SortName;
|
return ItemSortBy.SortName;
|
||||||
}
|
}
|
||||||
@@ -3520,6 +3541,13 @@ namespace Emby.Server.Implementations.Data
|
|||||||
statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
|
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)
|
if (query.MinDateCreated.HasValue)
|
||||||
{
|
{
|
||||||
whereClauses.Add("DateCreated>=@MinDateCreated");
|
whereClauses.Add("DateCreated>=@MinDateCreated");
|
||||||
@@ -3847,7 +3875,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@ArtistIds" + index;
|
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)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind(paramName, artistId);
|
statement.TryBind(paramName, artistId);
|
||||||
@@ -3868,7 +3896,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@ArtistIds" + index;
|
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)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind(paramName, artistId);
|
statement.TryBind(paramName, artistId);
|
||||||
@@ -3889,7 +3917,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@ArtistIds" + index;
|
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)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind(paramName, artistId);
|
statement.TryBind(paramName, artistId);
|
||||||
@@ -3931,7 +3959,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@ExcludeArtistId" + index;
|
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)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind(paramName, artistId);
|
statement.TryBind(paramName, artistId);
|
||||||
@@ -3952,7 +3980,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@GenreId" + index;
|
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)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind(paramName, genreId);
|
statement.TryBind(paramName, genreId);
|
||||||
@@ -3971,7 +3999,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var index = 0;
|
var index = 0;
|
||||||
foreach (var item in query.Genres)
|
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)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind("@Genre" + index, GetCleanValue(item));
|
statement.TryBind("@Genre" + index, GetCleanValue(item));
|
||||||
@@ -3990,7 +4018,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var index = 0;
|
var index = 0;
|
||||||
foreach (var item in tags)
|
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)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind("@Tag" + index, GetCleanValue(item));
|
statement.TryBind("@Tag" + index, GetCleanValue(item));
|
||||||
@@ -4009,7 +4037,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var index = 0;
|
var index = 0;
|
||||||
foreach (var item in excludeTags)
|
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)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
|
statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
|
||||||
@@ -4030,7 +4058,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
var paramName = "@StudioId" + index;
|
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)
|
if (statement != null)
|
||||||
{
|
{
|
||||||
@@ -4509,7 +4537,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
int index = 0;
|
int index = 0;
|
||||||
string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++));
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -4744,11 +4772,11 @@ namespace Emby.Server.Implementations.Data
|
|||||||
';',
|
';',
|
||||||
new string[]
|
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
|
FROM AncestorIds
|
||||||
LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
|
LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
|
||||||
where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 "
|
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)");
|
AND Type = @InternalPersonType)");
|
||||||
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
|
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
|
||||||
statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
|
statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
|
||||||
|
statement?.TryBind("@UserId", query.User.InternalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query.ItemId.Equals(default))
|
if (!query.ItemId.Equals(default))
|
||||||
@@ -4967,11 +4996,6 @@ AND Type = @InternalPersonType)");
|
|||||||
statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
|
statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.User != null)
|
|
||||||
{
|
|
||||||
statement?.TryBind("@UserId", query.User.InternalId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return whereClauses;
|
return whereClauses;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5763,7 +5787,7 @@ AND Type = @InternalPersonType)");
|
|||||||
{
|
{
|
||||||
var itemIdBlob = id.ToByteArray();
|
var itemIdBlob = id.ToByteArray();
|
||||||
|
|
||||||
// First delete chapters
|
// Delete existing mediastreams
|
||||||
db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob);
|
db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob);
|
||||||
|
|
||||||
InsertMediaStreams(itemIdBlob, streams, db);
|
InsertMediaStreams(itemIdBlob, streams, db);
|
||||||
@@ -5855,6 +5879,15 @@ AND Type = @InternalPersonType)");
|
|||||||
statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries);
|
statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries);
|
||||||
statement.TryBind("@ColorSpace" + index, stream.ColorSpace);
|
statement.TryBind("@ColorSpace" + index, stream.ColorSpace);
|
||||||
statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer);
|
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();
|
statement.Reset();
|
||||||
@@ -5867,10 +5900,10 @@ AND Type = @InternalPersonType)");
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the chapter.
|
/// Gets the media stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="reader">The reader.</param>
|
/// <param name="reader">The reader.</param>
|
||||||
/// <returns>ChapterInfo.</returns>
|
/// <returns>MediaStream.</returns>
|
||||||
private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader)
|
private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader)
|
||||||
{
|
{
|
||||||
var item = new MediaStream
|
var item = new MediaStream
|
||||||
@@ -6026,6 +6059,46 @@ AND Type = @InternalPersonType)");
|
|||||||
item.ColorTransfer = colorTransfer;
|
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)
|
if (item.Type == MediaStreamType.Subtitle)
|
||||||
{
|
{
|
||||||
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
if (options.ContainsField(ItemFields.People))
|
if (options.ContainsField(ItemFields.People))
|
||||||
{
|
{
|
||||||
AttachPeople(dto, item);
|
AttachPeople(dto, item, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
|
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
|
||||||
@@ -503,7 +503,8 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto">The dto.</param>
|
/// <param name="dto">The dto.</param>
|
||||||
/// <param name="item">The item.</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.
|
// Ordering by person type to ensure actors and artists are at the front.
|
||||||
// This is taking advantage of the fact that they both begin with A
|
// This is taking advantage of the fact that they both begin with A
|
||||||
@@ -560,6 +561,9 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).Where(i => i != null)
|
}).Where(i => i != null)
|
||||||
|
.Where(i => user == null ?
|
||||||
|
true :
|
||||||
|
i.IsVisible(user))
|
||||||
.GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
|
.GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(x => x.First())
|
.Select(x => x.First())
|
||||||
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -1094,7 +1098,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
{
|
{
|
||||||
if (item is IHasTrailers hasTrailers)
|
if (item is IHasTrailers hasTrailers)
|
||||||
{
|
{
|
||||||
dto.LocalTrailerCount = hasTrailers.GetTrailerCount();
|
dto.LocalTrailerCount = hasTrailers.LocalTrailers.Count;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,15 +24,15 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
|
<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.DependencyInjection" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.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="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
|
||||||
<PackageReference Include="Mono.Nat" Version="3.0.2" />
|
<PackageReference Include="Mono.Nat" Version="3.0.3" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.3" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
|
||||||
<PackageReference Include="sharpcompress" Version="0.30.1" />
|
<PackageReference Include="sharpcompress" Version="0.32.2" />
|
||||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class WebSocketConnection.
|
/// Class WebSocketConnection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WebSocketConnection : IWebSocketConnection, IDisposable
|
public class WebSocketConnection : IWebSocketConnection
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The logger.
|
/// The logger.
|
||||||
@@ -36,6 +36,8 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly WebSocket _socket;
|
private readonly WebSocket _socket;
|
||||||
|
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
|
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
|
||||||
/// </summary>
|
/// </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>
|
/// <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)
|
protected virtual void Dispose(bool dispose)
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (dispose)
|
if (dispose)
|
||||||
{
|
{
|
||||||
_socket.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);
|
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
|
||||||
result.Exists = false;
|
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));
|
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
|
// Skip image processing if current or live tv source
|
||||||
if (outdated.Length == 0 || item.SourceType != SourceType.Library)
|
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);
|
_logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
|
||||||
continue;
|
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);
|
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
|
||||||
continue;
|
continue;
|
||||||
@@ -1895,23 +1897,24 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImageDimensions size;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
|
size = _imageProcessor.GetImageDimensions(item, image);
|
||||||
image.Width = size.Width;
|
image.Width = size.Width;
|
||||||
image.Height = size.Height;
|
image.Height = size.Height;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
||||||
|
size = new ImageDimensions(0, 0);
|
||||||
image.Width = 0;
|
image.Width = 0;
|
||||||
image.Height = 0;
|
image.Height = 0;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path);
|
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -2450,6 +2453,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return RootFolder;
|
return RootFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void QueueLibraryScan()
|
||||||
|
{
|
||||||
|
_taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int? GetSeasonNumberFromPath(string path)
|
public int? GetSeasonNumberFromPath(string path)
|
||||||
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
|
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
|
||||||
@@ -2757,7 +2766,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public List<Person> GetPeopleItems(InternalPeopleQuery query)
|
public List<Person> GetPeopleItems(InternalPeopleQuery query)
|
||||||
{
|
{
|
||||||
return _itemRepository.GetPeopleNames(query).Select(i =>
|
return _itemRepository.GetPeopleNames(query)
|
||||||
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -2768,7 +2778,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_logger.LogError(ex, "Error getting person");
|
_logger.LogError(ex, "Error getting person");
|
||||||
return null;
|
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)
|
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 existingNameCount = 1; // first numbered name will be 2
|
||||||
var virtualFolderPath = Path.Combine(rootFolderPath, name);
|
var virtualFolderPath = Path.Combine(rootFolderPath, name);
|
||||||
|
var originalName = name;
|
||||||
while (Directory.Exists(virtualFolderPath))
|
while (Directory.Exists(virtualFolderPath))
|
||||||
{
|
{
|
||||||
existingNameCount++;
|
existingNameCount++;
|
||||||
virtualFolderPath = Path.Combine(rootFolderPath, name + " " + existingNameCount);
|
name = originalName + existingNameCount;
|
||||||
|
virtualFolderPath = Path.Combine(rootFolderPath, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaPathInfos = options.PathInfos;
|
var mediaPathInfos = options.PathInfos;
|
||||||
|
|||||||
@@ -151,7 +151,11 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
|
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(
|
await item.RefreshMetadata(
|
||||||
new MetadataRefreshOptions(_directoryService)
|
new MetadataRefreshOptions(_directoryService)
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
public static int? GetDefaultAudioStreamIndex(IReadOnlyList<MediaStream> streams, IReadOnlyList<string> preferredLanguages, bool preferDefaultTrack)
|
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)
|
if (preferDefaultTrack)
|
||||||
{
|
{
|
||||||
var defaultStream = streams.FirstOrDefault(i => i.IsDefault);
|
var defaultStream = sortedStreams.FirstOrDefault(i => i.IsDefault);
|
||||||
|
|
||||||
if (defaultStream != null)
|
if (defaultStream != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
|
|
||||||
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
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;
|
return null;
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Diacritics.Extensions;
|
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
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);
|
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)
|
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
|
// 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);
|
_ = 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)
|
private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||||
{
|
{
|
||||||
@@ -48,12 +49,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(info.EpisodeTitle))
|
if (!string.IsNullOrWhiteSpace(info.EpisodeTitle))
|
||||||
{
|
{
|
||||||
|
var tmpName = name;
|
||||||
if (addHyphen)
|
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)
|
else if (info.IsMovie && info.ProductionYear != null)
|
||||||
|
|||||||
@@ -165,12 +165,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
|
|
||||||
const double DesiredAspect = 2.0 / 3;
|
const double DesiredAspect = 2.0 / 3;
|
||||||
|
|
||||||
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ??
|
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect, token) ??
|
||||||
GetProgramImage(ApiUrl, allImages, DesiredAspect);
|
GetProgramImage(ApiUrl, allImages, DesiredAspect, token);
|
||||||
|
|
||||||
const double WideAspect = 16.0 / 9;
|
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
|
// Don't supply the same image twice
|
||||||
if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
|
if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
|
||||||
@@ -178,7 +178,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
programEntry.ThumbImage = null;
|
programEntry.ThumbImage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect);
|
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect, token);
|
||||||
|
|
||||||
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
|
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
|
||||||
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
|
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
|
||||||
@@ -399,7 +399,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
return info;
|
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
|
var match = images
|
||||||
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
|
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
|
||||||
@@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return apiUrl + "/image/" + uri;
|
return apiUrl + "/image/" + uri + "?token=" + token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +457,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
IReadOnlyList<string> programIds,
|
IReadOnlyList<string> programIds,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (programIds.Count == 0)
|
if (programIds.Count == 0)
|
||||||
{
|
{
|
||||||
return Array.Empty<ShowImagesDto>();
|
return Array.Empty<ShowImagesDto>();
|
||||||
@@ -478,6 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
{
|
{
|
||||||
Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json)
|
Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json)
|
||||||
};
|
};
|
||||||
|
message.Headers.TryAddWithoutValidation("token", token);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
@@ -26,6 +27,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
{
|
{
|
||||||
public class XmlTvListingsProvider : IListingsProvider
|
public class XmlTvListingsProvider : IListingsProvider
|
||||||
{
|
{
|
||||||
|
private static readonly TimeSpan _maxCacheAge = TimeSpan.FromHours(1);
|
||||||
|
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly ILogger<XmlTvListingsProvider> _logger;
|
private readonly ILogger<XmlTvListingsProvider> _logger;
|
||||||
@@ -69,13 +72,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
return UnzipIfNeeded(info.Path, info.Path);
|
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);
|
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);
|
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);
|
_logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
|
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
|
||||||
@@ -124,7 +133,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
{
|
{
|
||||||
using (var stream = File.OpenRead(file))
|
using (var stream = File.OpenRead(file))
|
||||||
{
|
{
|
||||||
string tempFolder = Path.Combine(_config.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString());
|
string tempFolder = GetTempFolderPath(stream);
|
||||||
Directory.CreateDirectory(tempFolder);
|
Directory.CreateDirectory(tempFolder);
|
||||||
|
|
||||||
_zipClient.ExtractFirstFileFromGz(stream, tempFolder, "data.xml");
|
_zipClient.ExtractFirstFileFromGz(stream, tempFolder, "data.xml");
|
||||||
@@ -137,7 +146,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
{
|
{
|
||||||
using (var stream = File.OpenRead(file))
|
using (var stream = File.OpenRead(file))
|
||||||
{
|
{
|
||||||
string tempFolder = Path.Combine(_config.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString());
|
string tempFolder = GetTempFolderPath(stream);
|
||||||
Directory.CreateDirectory(tempFolder);
|
Directory.CreateDirectory(tempFolder);
|
||||||
|
|
||||||
_zipClient.ExtractAllFromGz(stream, tempFolder, true);
|
_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)
|
private string FindXmlFile(string directory)
|
||||||
{
|
{
|
||||||
return _fileSystem.GetFiles(directory, true)
|
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);
|
int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none");
|
return VerifyReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), "none");
|
||||||
}
|
}
|
||||||
finally
|
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 (Dominicana)", "es_DO");
|
||||||
yield return new LocalizationOption("Español (México)", "es-MX");
|
yield return new LocalizationOption("Español (México)", "es-MX");
|
||||||
yield return new LocalizationOption("Eesti", "et");
|
yield return new LocalizationOption("Eesti", "et");
|
||||||
|
yield return new LocalizationOption("Basque", "eu");
|
||||||
yield return new LocalizationOption("فارسی", "fa");
|
yield return new LocalizationOption("فارسی", "fa");
|
||||||
yield return new LocalizationOption("Suomi", "fi");
|
yield return new LocalizationOption("Suomi", "fi");
|
||||||
yield return new LocalizationOption("Filipino", "fil");
|
yield return new LocalizationOption("Filipino", "fil");
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error updating {0}", package.Name);
|
_logger.LogError(ex, "Error updating {0}", package.Name);
|
||||||
}
|
}
|
||||||
|
catch (InvalidDataException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error updating {0}", package.Name);
|
||||||
|
}
|
||||||
|
|
||||||
// Update progress
|
// Update progress
|
||||||
lock (progress)
|
lock (progress)
|
||||||
|
|||||||
@@ -329,13 +329,17 @@ namespace Emby.Server.Implementations.Session
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void CloseIfNeeded(SessionInfo session)
|
public async Task CloseIfNeededAsync(SessionInfo session)
|
||||||
{
|
{
|
||||||
if (!session.SessionControllers.Any(i => i.IsSessionActive))
|
if (!session.SessionControllers.Any(i => i.IsSessionActive))
|
||||||
{
|
{
|
||||||
var key = GetSessionKey(session.Client, session.DeviceId);
|
var key = GetSessionKey(session.Client, session.DeviceId);
|
||||||
|
|
||||||
_activeConnections.TryRemove(key, out _);
|
_activeConnections.TryRemove(key, out _);
|
||||||
|
if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
|
||||||
|
{
|
||||||
|
await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
OnSessionEnded(session);
|
OnSessionEnded(session);
|
||||||
}
|
}
|
||||||
@@ -413,6 +417,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
session.PlayState.IsPaused = info.IsPaused;
|
session.PlayState.IsPaused = info.IsPaused;
|
||||||
session.PlayState.PositionTicks = info.PositionTicks;
|
session.PlayState.PositionTicks = info.PositionTicks;
|
||||||
session.PlayState.MediaSourceId = info.MediaSourceId;
|
session.PlayState.MediaSourceId = info.MediaSourceId;
|
||||||
|
session.PlayState.LiveStreamId = info.LiveStreamId;
|
||||||
session.PlayState.CanSeek = info.CanSeek;
|
session.PlayState.CanSeek = info.CanSeek;
|
||||||
session.PlayState.IsMuted = info.IsMuted;
|
session.PlayState.IsMuted = info.IsMuted;
|
||||||
session.PlayState.VolumeLevel = info.VolumeLevel;
|
session.PlayState.VolumeLevel = info.VolumeLevel;
|
||||||
@@ -699,7 +704,9 @@ namespace Emby.Server.Implementations.Session
|
|||||||
DeviceName = session.DeviceName,
|
DeviceName = session.DeviceName,
|
||||||
ClientName = session.Client,
|
ClientName = session.Client,
|
||||||
DeviceId = session.DeviceId,
|
DeviceId = session.DeviceId,
|
||||||
Session = session
|
Session = session,
|
||||||
|
PlaybackPositionTicks = info.PositionTicks,
|
||||||
|
PlaySessionId = info.PlaySessionId
|
||||||
};
|
};
|
||||||
|
|
||||||
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
|
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
|
||||||
@@ -768,6 +775,11 @@ namespace Emby.Server.Implementations.Session
|
|||||||
|
|
||||||
await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
|
await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
|
||||||
|
{
|
||||||
|
ClearTranscodingInfo(session.DeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
var users = GetUsers(session);
|
var users = GetUsers(session);
|
||||||
|
|
||||||
// only update saved user data on actual check-ins, not automated ones
|
// only update saved user data on actual check-ins, not automated ones
|
||||||
@@ -985,7 +997,8 @@ namespace Emby.Server.Implementations.Session
|
|||||||
DeviceName = session.DeviceName,
|
DeviceName = session.DeviceName,
|
||||||
ClientName = session.Client,
|
ClientName = session.Client,
|
||||||
DeviceId = session.DeviceId,
|
DeviceId = session.DeviceId,
|
||||||
Session = session
|
Session = session,
|
||||||
|
PlaySessionId = info.PlaySessionId
|
||||||
};
|
};
|
||||||
|
|
||||||
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
|
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Emby.Server.Implementations.Session
|
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 ILogger<WebSocketController> _logger;
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
@@ -53,13 +53,13 @@ namespace Emby.Server.Implementations.Session
|
|||||||
connection.Closed += OnConnectionClosed;
|
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));
|
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);
|
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
|
||||||
_sockets.Remove(connection);
|
_sockets.Remove(connection);
|
||||||
connection.Closed -= OnConnectionClosed;
|
connection.Closed -= OnConnectionClosed;
|
||||||
_sessionManager.CloseIfNeeded(_session);
|
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -99,6 +99,23 @@ namespace Emby.Server.Implementations.Session
|
|||||||
foreach (var socket in _sockets)
|
foreach (var socket in _sockets)
|
||||||
{
|
{
|
||||||
socket.Closed -= OnConnectionClosed;
|
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;
|
_disposed = true;
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ namespace Emby.Server.Implementations.Sorting
|
|||||||
throw new ArgumentNullException(nameof(y));
|
throw new ArgumentNullException(nameof(y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!x.IndexNumber.HasValue && !y.IndexNumber.HasValue)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (!x.IndexNumber.HasValue)
|
if (!x.IndexNumber.HasValue)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ namespace Emby.Server.Implementations.Sorting
|
|||||||
throw new ArgumentNullException(nameof(y));
|
throw new ArgumentNullException(nameof(y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!x.ParentIndexNumber.HasValue && !y.ParentIndexNumber.HasValue)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (!x.ParentIndexNumber.HasValue)
|
if (!x.ParentIndexNumber.HasValue)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -135,20 +135,15 @@ namespace Emby.Server.Implementations.TV
|
|||||||
return GetResult(episodes, request);
|
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 allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false));
|
||||||
var currentUser = user;
|
|
||||||
|
|
||||||
var allNextUp = seriesKeys
|
|
||||||
.Select(i => GetNextUp(i, currentUser, dtoOptions, false));
|
|
||||||
|
|
||||||
if (request.EnableRewatching)
|
if (request.EnableRewatching)
|
||||||
{
|
{
|
||||||
allNextUp = allNextUp.Concat(
|
allNextUp = allNextUp.Concat(
|
||||||
seriesKeys.Select(i => GetNextUp(i, currentUser, dtoOptions, true))
|
seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true)))
|
||||||
)
|
.OrderByDescending(i => i.LastWatchedDate);
|
||||||
.OrderByDescending(i => i.Item1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If viewing all next up for all series, remove first episodes
|
// If viewing all next up for all series, remove first episodes
|
||||||
@@ -161,23 +156,18 @@ namespace Emby.Server.Implementations.TV
|
|||||||
{
|
{
|
||||||
if (request.DisableFirstEpisode)
|
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;
|
anyFound = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!anyFound && i.Item1 == DateTime.MinValue)
|
return !anyFound && i.LastWatchedDate == DateTime.MinValue;
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
})
|
})
|
||||||
.Select(i => i.Item2())
|
.Select(i => i.GetEpisodeFunction())
|
||||||
.Where(i => i != null);
|
.Where(i => i != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,14 +185,13 @@ namespace Emby.Server.Implementations.TV
|
|||||||
/// Gets the next up.
|
/// Gets the next up.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Task{Episode}.</returns>
|
/// <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)
|
var lastQuery = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
AncestorWithPresentationUniqueKey = null,
|
AncestorWithPresentationUniqueKey = null,
|
||||||
SeriesPresentationUniqueKey = seriesKey,
|
SeriesPresentationUniqueKey = seriesKey,
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Descending) },
|
|
||||||
IsPlayed = true,
|
IsPlayed = true,
|
||||||
Limit = 1,
|
Limit = 1,
|
||||||
ParentIndexNumberNotEquals = 0,
|
ParentIndexNumberNotEquals = 0,
|
||||||
@@ -213,42 +202,38 @@ namespace Emby.Server.Implementations.TV
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (rewatching)
|
// If rewatching is enabled, sort first by date played and then by season and episode numbers
|
||||||
{
|
lastQuery.OrderBy = rewatching
|
||||||
// find last watched by date played, not by newest episode watched
|
? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
|
||||||
lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) };
|
: new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
|
||||||
}
|
|
||||||
|
|
||||||
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
|
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
|
||||||
|
|
||||||
Func<Episode> getEpisode = () =>
|
Episode GetEpisode()
|
||||||
{
|
{
|
||||||
var nextQuery = new InternalItemsQuery(user)
|
var nextQuery = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
AncestorWithPresentationUniqueKey = null,
|
AncestorWithPresentationUniqueKey = null,
|
||||||
SeriesPresentationUniqueKey = seriesKey,
|
SeriesPresentationUniqueKey = seriesKey,
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
|
||||||
Limit = 1,
|
Limit = 1,
|
||||||
IsPlayed = rewatching,
|
IsPlayed = rewatching,
|
||||||
IsVirtualItem = false,
|
IsVirtualItem = false,
|
||||||
ParentIndexNumberNotEquals = 0,
|
ParentIndexNumberNotEquals = 0,
|
||||||
MinSortName = lastWatchedEpisode?.SortName,
|
|
||||||
DtoOptions = dtoOptions
|
DtoOptions = dtoOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
Episode nextEpisode;
|
// Locate the next up episode based on the last watched episode's season and episode number
|
||||||
if (rewatching)
|
var lastWatchedParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber;
|
||||||
|
var lastWatchedIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber;
|
||||||
|
if (lastWatchedParentIndexNumber.HasValue && lastWatchedIndexNumber.HasValue)
|
||||||
{
|
{
|
||||||
nextQuery.Limit = 2;
|
nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1);
|
||||||
// get watched episode after most recently watched
|
|
||||||
nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().ElementAtOrDefault(1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
|
||||||
|
|
||||||
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
|
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
|
||||||
{
|
{
|
||||||
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||||
@@ -297,7 +282,7 @@ namespace Emby.Server.Implementations.TV
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nextEpisode;
|
return nextEpisode;
|
||||||
};
|
}
|
||||||
|
|
||||||
if (lastWatchedEpisode != null)
|
if (lastWatchedEpisode != null)
|
||||||
{
|
{
|
||||||
@@ -305,11 +290,11 @@ namespace Emby.Server.Implementations.TV
|
|||||||
|
|
||||||
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
|
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
|
||||||
|
|
||||||
return new Tuple<DateTime, Func<Episode>>(lastWatchedDate, getEpisode);
|
return (lastWatchedDate, GetEpisode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the first episode
|
// 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)
|
private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)
|
||||||
|
|||||||
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="ignoreSchedule">Whether to ignore parental control.</param>
|
||||||
/// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
|
/// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
|
||||||
/// <param name="requiredDownloadPermission">Whether validation requires download permission.</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>
|
/// <returns>Validated claim status.</returns>
|
||||||
protected bool ValidateClaims(
|
protected bool ValidateClaims(
|
||||||
ClaimsPrincipal claimsPrincipal,
|
ClaimsPrincipal claimsPrincipal,
|
||||||
bool ignoreSchedule = false,
|
bool ignoreSchedule = false,
|
||||||
bool localAccessOnly = false,
|
bool localAccessOnly = false,
|
||||||
bool requiredDownloadPermission = false)
|
bool requiredDownloadPermission = false,
|
||||||
|
bool requireLiveTvManagementPermission = false,
|
||||||
|
bool requireLiveTvAccessPermission = false)
|
||||||
{
|
{
|
||||||
// ApiKey is currently global admin, always allow.
|
// ApiKey is currently global admin, always allow.
|
||||||
var isApiKey = ClaimHelpers.GetIsApiKey(claimsPrincipal);
|
var isApiKey = ClaimHelpers.GetIsApiKey(claimsPrincipal);
|
||||||
@@ -106,6 +110,20 @@ namespace Jellyfin.Api.Auth
|
|||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace Jellyfin.Api.Auth.DownloadPolicy
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement)
|
||||||
{
|
{
|
||||||
var validated = ValidateClaims(context.User);
|
var validated = ValidateClaims(context.User, requiredDownloadPermission: true);
|
||||||
if (validated)
|
if (validated)
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
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.
|
/// Policy name for accessing a SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
|
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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,21 +86,23 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// Updates named configuration.
|
/// Updates named configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">Configuration key.</param>
|
/// <param name="key">Configuration key.</param>
|
||||||
|
/// <param name="configuration">Configuration.</param>
|
||||||
/// <response code="204">Named configuration updated.</response>
|
/// <response code="204">Named configuration updated.</response>
|
||||||
/// <returns>Update status.</returns>
|
/// <returns>Update status.</returns>
|
||||||
[HttpPost("Configuration/{key}")]
|
[HttpPost("Configuration/{key}")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[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 configurationType = _configurationManager.GetConfigurationType(key);
|
||||||
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false);
|
var deserializedConfiguration = configuration.Deserialize(configurationType, _serializerOptions);
|
||||||
if (configuration == null)
|
|
||||||
|
if (deserializedConfiguration == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Body doesn't contain a valid configuration");
|
throw new ArgumentException("Body doesn't contain a valid configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
_configurationManager.SaveConfiguration(key, configuration);
|
_configurationManager.SaveConfiguration(key, deserializedConfiguration);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// Dlna Server Controller.
|
/// Dlna Server Controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Dlna")]
|
[Route("Dlna")]
|
||||||
|
[DlnaEnabled]
|
||||||
[Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
|
[Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
|
||||||
public class DlnaServerController : BaseJellyfinApiController
|
public class DlnaServerController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
@@ -55,15 +56,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
|
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
var url = GetAbsoluteUri();
|
||||||
{
|
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
||||||
var url = GetAbsoluteUri();
|
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
|
||||||
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
return Ok(xml);
|
||||||
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
|
|
||||||
return Ok(xml);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -83,12 +79,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
|
public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
return Ok(_contentDirectory.GetServiceXml());
|
||||||
{
|
|
||||||
return Ok(_contentDirectory.GetServiceXml());
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -108,12 +99,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
|
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
||||||
{
|
|
||||||
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -133,12 +119,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
|
public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
return Ok(_connectionManager.GetServiceXml());
|
||||||
{
|
|
||||||
return Ok(_connectionManager.GetServiceXml());
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -155,12 +136,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
|
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
||||||
{
|
|
||||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -177,12 +153,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
|
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
||||||
{
|
|
||||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -199,12 +170,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
|
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
||||||
{
|
|
||||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -224,12 +190,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
return ProcessEventRequest(_mediaReceiverRegistrar);
|
||||||
{
|
|
||||||
return ProcessEventRequest(_mediaReceiverRegistrar);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -249,12 +210,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
return ProcessEventRequest(_contentDirectory);
|
||||||
{
|
|
||||||
return ProcessEventRequest(_contentDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -274,12 +230,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
return ProcessEventRequest(_connectionManager);
|
||||||
{
|
|
||||||
return ProcessEventRequest(_connectionManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -299,12 +250,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
|
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
return GetIconInternal(fileName);
|
||||||
{
|
|
||||||
return GetIconInternal(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -322,12 +268,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
public ActionResult GetIcon([FromRoute, Required] string fileName)
|
public ActionResult GetIcon([FromRoute, Required] string fileName)
|
||||||
{
|
{
|
||||||
if (DlnaEntryPoint.Enabled)
|
return GetIconInternal(fileName);
|
||||||
{
|
|
||||||
return GetIconInternal(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ActionResult GetIconInternal(string fileName)
|
private ActionResult GetIconInternal(string fileName)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Jellyfin.Api.Constants;
|
|||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.Models.PlaybackDtos;
|
using Jellyfin.Api.Models.PlaybackDtos;
|
||||||
using Jellyfin.Api.Models.StreamingDtos;
|
using Jellyfin.Api.Models.StreamingDtos;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using Jellyfin.MediaEncoding.Hls.Playlist;
|
using Jellyfin.MediaEncoding.Hls.Playlist;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
@@ -21,6 +22,7 @@ using MediaBrowser.Controller.Dlna;
|
|||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
|
using MediaBrowser.MediaEncoding.Encoder;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
@@ -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
|
// 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
|
// since it gets disposed when ffmpeg exits
|
||||||
var cancellationToken = cancellationTokenSource.Token;
|
var cancellationToken = cancellationTokenSource.Token;
|
||||||
using var state = await StreamingHelpers.GetStreamingState(
|
var state = await StreamingHelpers.GetStreamingState(
|
||||||
streamingRequest,
|
streamingRequest,
|
||||||
Request,
|
Request,
|
||||||
_authContext,
|
_authContext,
|
||||||
@@ -1414,7 +1416,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
state.RunTimeTicks ?? 0,
|
state.RunTimeTicks ?? 0,
|
||||||
state.Request.SegmentContainer ?? string.Empty,
|
state.Request.SegmentContainer ?? string.Empty,
|
||||||
"hls1/main/",
|
"hls1/main/",
|
||||||
Request.QueryString.ToString());
|
Request.QueryString.ToString(),
|
||||||
|
EncodingHelper.IsCopyCodec(state.OutputVideoCodec));
|
||||||
var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
|
var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
|
||||||
|
|
||||||
return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
|
return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
|
||||||
@@ -1431,7 +1434,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
var cancellationTokenSource = new CancellationTokenSource();
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
var cancellationToken = cancellationTokenSource.Token;
|
var cancellationToken = cancellationTokenSource.Token;
|
||||||
|
|
||||||
using var state = await StreamingHelpers.GetStreamingState(
|
var state = await StreamingHelpers.GetStreamingState(
|
||||||
streamingRequest,
|
streamingRequest,
|
||||||
Request,
|
Request,
|
||||||
_authContext,
|
_authContext,
|
||||||
@@ -1661,8 +1664,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
startNumber.ToString(CultureInfo.InvariantCulture),
|
startNumber.ToString(CultureInfo.InvariantCulture),
|
||||||
baseUrlParam,
|
baseUrlParam,
|
||||||
isEventPlaylist ? "event" : "vod",
|
isEventPlaylist ? "event" : "vod",
|
||||||
outputTsArg,
|
EncodingUtils.NormalizePath(outputTsArg),
|
||||||
outputPath).Trim();
|
EncodingUtils.NormalizePath(outputPath)).Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1692,7 +1695,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
audioTranscodeParams += "-acodec " + audioCodec;
|
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);
|
audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
@@ -1711,20 +1714,32 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return audioTranscodeParams;
|
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))
|
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||||
{
|
{
|
||||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
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))
|
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;
|
var channels = state.OutputAudioChannels;
|
||||||
|
|
||||||
@@ -1734,8 +1749,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
var bitrate = state.OutputAudioBitrate;
|
var bitrate = state.OutputAudioBitrate;
|
||||||
|
if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
|
||||||
if (bitrate.HasValue)
|
|
||||||
{
|
{
|
||||||
args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
|
args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
@@ -1773,13 +1787,25 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
var args = "-codec:v:0 " + codec;
|
var args = "-codec:v:0 " + codec;
|
||||||
|
|
||||||
// Prefer hvc1 to hev1.
|
|
||||||
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(codec, "hevc", 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)
|
// 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";
|
// args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
|
||||||
|
|
||||||
// video processing filters.
|
// 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,
|
// -start_at_zero is necessary to use with -ss when seeking,
|
||||||
// otherwise the target position cannot be determined.
|
// otherwise the target position cannot be determined.
|
||||||
|
|||||||
@@ -1724,6 +1724,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery, Range(0, 100)] int quality = 90)
|
[FromQuery, Range(0, 100)] int quality = 90)
|
||||||
{
|
{
|
||||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||||
|
if (!brandingOptions.SplashscreenEnabled)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
string splashscreenPath;
|
string splashscreenPath;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
|
if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
|
||||||
@@ -1776,6 +1781,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Uploads a custom splashscreen.
|
/// Uploads a custom splashscreen.
|
||||||
|
/// The body is expected to the image contents base64 encoded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
/// <response code="204">Successfully uploaded new splashscreen.</response>
|
/// <response code="204">Successfully uploaded new splashscreen.</response>
|
||||||
@@ -1799,7 +1805,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return BadRequest("Error reading mimetype from uploaded image");
|
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");
|
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||||
brandingOptions.SplashscreenLocation = filePath;
|
brandingOptions.SplashscreenLocation = filePath;
|
||||||
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
|
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
|
||||||
@@ -1812,6 +1824,29 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return NoContent();
|
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)
|
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(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="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="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="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="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="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="limit">Optional. The maximum number of records to return.</param>
|
||||||
@@ -173,6 +178,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? hasImdbId,
|
[FromQuery] bool? hasImdbId,
|
||||||
[FromQuery] bool? hasTmdbId,
|
[FromQuery] bool? hasTmdbId,
|
||||||
[FromQuery] bool? hasTvdbId,
|
[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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||||
[FromQuery] int? startIndex,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
@@ -260,30 +270,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
includeItemTypes = new[] { BaseItemKind.Playlist };
|
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
|
if (item is not UserRootFolder
|
||||||
&& !isInEnabledFolder
|
// api keys can always access all folders
|
||||||
&& !user.HasPermission(PermissionKind.EnableAllFolders)
|
&& !ClaimHelpers.GetIsApiKey(User)
|
||||||
&& !user.HasPermission(PermissionKind.EnableAllChannels)
|
// check the item is visible for the user
|
||||||
&& !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
|
&& !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}.");
|
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +309,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Is3D = is3D,
|
Is3D = is3D,
|
||||||
HasTvdbId = hasTvdbId,
|
HasTvdbId = hasTvdbId,
|
||||||
HasTmdbId = hasTmdbId,
|
HasTmdbId = hasTmdbId,
|
||||||
|
IsMovie = isMovie,
|
||||||
|
IsSeries = isSeries,
|
||||||
|
IsNews = isNews,
|
||||||
|
IsKids = isKids,
|
||||||
|
IsSports = isSports,
|
||||||
HasOverview = hasOverview,
|
HasOverview = hasOverview,
|
||||||
HasOfficialRating = hasOfficialRating,
|
HasOfficialRating = hasOfficialRating,
|
||||||
HasParentalRating = hasParentalRating,
|
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="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="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="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="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 delimeted.</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="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="isUnaired">Optional filter by items that are unaired episodes or not.</param>
|
||||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</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="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="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="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="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="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="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="searchTerm">Optional. Filter based on a search term.</param>
|
||||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</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="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="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 delimeted.</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 delimeted.</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 delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</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="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="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="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="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="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 delimeted.</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 delimeted.</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 delimeted.</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="enableUserData">Optional, include user data.</param>
|
||||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</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="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="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="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="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="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 delimeted.</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 delimeted.</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="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="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="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="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 delimeted.</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="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="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="isLocked">Optional filter by items that are locked.</param>
|
||||||
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</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="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="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="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="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="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="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="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 delimeted.</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="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
||||||
@@ -613,6 +616,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? hasImdbId,
|
[FromQuery] bool? hasImdbId,
|
||||||
[FromQuery] bool? hasTmdbId,
|
[FromQuery] bool? hasTmdbId,
|
||||||
[FromQuery] bool? hasTvdbId,
|
[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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||||
[FromQuery] int? startIndex,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
@@ -695,6 +703,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
hasImdbId,
|
hasImdbId,
|
||||||
hasTmdbId,
|
hasTmdbId,
|
||||||
hasTvdbId,
|
hasTvdbId,
|
||||||
|
isMovie,
|
||||||
|
isSeries,
|
||||||
|
isNews,
|
||||||
|
isKids,
|
||||||
|
isSports,
|
||||||
excludeItemIds,
|
excludeItemIds,
|
||||||
startIndex,
|
startIndex,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Media folders returned.</response>
|
/// <response code="200">Media folders returned.</response>
|
||||||
/// <returns>List of user media folders.</returns>
|
/// <returns>List of user media folders.</returns>
|
||||||
[HttpGet("Library/MediaFolders")]
|
[HttpGet("Library/MediaFolders")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
|
public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Info")]
|
[HttpGet("Info")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
public ActionResult<LiveTvInfo> GetLiveTvInfo()
|
public ActionResult<LiveTvInfo> GetLiveTvInfo()
|
||||||
{
|
{
|
||||||
return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
|
return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
|
||||||
@@ -129,7 +129,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Channels")]
|
[HttpGet("Channels")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
|
public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
|
||||||
[FromQuery] ChannelType? type,
|
[FromQuery] ChannelType? type,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
@@ -208,7 +208,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
|
||||||
[HttpGet("Channels/{channelId}")]
|
[HttpGet("Channels/{channelId}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
|
public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
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>
|
/// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
|
||||||
[HttpGet("Recordings")]
|
[HttpGet("Recordings")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
|
public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
|
||||||
[FromQuery] string? channelId,
|
[FromQuery] string? channelId,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
@@ -320,7 +320,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
|
||||||
[HttpGet("Recordings/Series")]
|
[HttpGet("Recordings/Series")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[Obsolete("This endpoint is obsolete.")]
|
[Obsolete("This endpoint is obsolete.")]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", 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>
|
/// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
|
||||||
[HttpGet("Recordings/Groups")]
|
[HttpGet("Recordings/Groups")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[Obsolete("This endpoint is obsolete.")]
|
[Obsolete("This endpoint is obsolete.")]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
|
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>
|
/// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
|
||||||
[HttpGet("Recordings/Folders")]
|
[HttpGet("Recordings/Folders")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
|
public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
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>
|
/// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
|
||||||
[HttpGet("Recordings/{recordingId}")]
|
[HttpGet("Recordings/{recordingId}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
|
public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
|
||||||
{
|
{
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
@@ -423,10 +423,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Tuners/{tunerId}/Reset")]
|
[HttpPost("Tuners/{tunerId}/Reset")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
|
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
|
||||||
{
|
{
|
||||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
|
||||||
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
|
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -441,7 +440,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Timers/{timerId}")]
|
[HttpGet("Timers/{timerId}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
|
public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
|
||||||
{
|
{
|
||||||
return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
|
return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
|
||||||
@@ -457,7 +456,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Timers/Defaults")]
|
[HttpGet("Timers/Defaults")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
|
public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(programId)
|
return string.IsNullOrEmpty(programId)
|
||||||
@@ -477,7 +476,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Timers")]
|
[HttpGet("Timers")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
|
public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
|
||||||
[FromQuery] string? channelId,
|
[FromQuery] string? channelId,
|
||||||
[FromQuery] string? seriesTimerId,
|
[FromQuery] string? seriesTimerId,
|
||||||
@@ -531,7 +530,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Programs")]
|
[HttpGet("Programs")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
|
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
@@ -614,7 +613,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpPost("Programs")]
|
[HttpPost("Programs")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
|
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
|
||||||
{
|
{
|
||||||
var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
|
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>
|
/// <response code="200">Recommended epgs returned.</response>
|
||||||
/// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
|
/// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
|
||||||
[HttpGet("Programs/Recommended")]
|
[HttpGet("Programs/Recommended")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
|
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
@@ -732,7 +731,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Program returned.</response>
|
/// <response code="200">Program returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
|
||||||
[HttpGet("Programs/{programId}")]
|
[HttpGet("Programs/{programId}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<BaseItemDto>> GetProgram(
|
public async Task<ActionResult<BaseItemDto>> GetProgram(
|
||||||
[FromRoute, Required] string programId,
|
[FromRoute, Required] string programId,
|
||||||
@@ -753,13 +752,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="404">Item not found.</response>
|
/// <response code="404">Item not found.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||||
[HttpDelete("Recordings/{recordingId}")]
|
[HttpDelete("Recordings/{recordingId}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[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);
|
var item = _libraryManager.GetItemById(recordingId);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
{
|
{
|
||||||
@@ -781,11 +778,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="204">Timer deleted.</response>
|
/// <response code="204">Timer deleted.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpDelete("Timers/{timerId}")]
|
[HttpDelete("Timers/{timerId}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
|
public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
|
||||||
{
|
{
|
||||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
|
||||||
await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
|
await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -798,12 +794,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="204">Timer updated.</response>
|
/// <response code="204">Timer updated.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Timers/{timerId}")]
|
[HttpPost("Timers/{timerId}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
||||||
public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
|
public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
|
||||||
{
|
{
|
||||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
|
||||||
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -815,11 +810,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="204">Timer created.</response>
|
/// <response code="204">Timer created.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Timers")]
|
[HttpPost("Timers")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
|
public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
|
||||||
{
|
{
|
||||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
|
||||||
await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -832,7 +826,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="404">Series timer not found.</response>
|
/// <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>
|
/// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
|
||||||
[HttpGet("SeriesTimers/{timerId}")]
|
[HttpGet("SeriesTimers/{timerId}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
|
public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
|
||||||
@@ -854,7 +848,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Timers returned.</response>
|
/// <response code="200">Timers returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
|
/// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
|
||||||
[HttpGet("SeriesTimers")]
|
[HttpGet("SeriesTimers")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
|
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>
|
/// <response code="204">Timer cancelled.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpDelete("SeriesTimers/{timerId}")]
|
[HttpDelete("SeriesTimers/{timerId}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
|
public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
|
||||||
{
|
{
|
||||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
|
||||||
await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
|
await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -891,12 +884,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="204">Series timer updated.</response>
|
/// <response code="204">Series timer updated.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("SeriesTimers/{timerId}")]
|
[HttpPost("SeriesTimers/{timerId}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
||||||
public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
||||||
{
|
{
|
||||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
|
||||||
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -908,11 +900,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="204">Series timer info created.</response>
|
/// <response code="204">Series timer info created.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("SeriesTimers")]
|
[HttpPost("SeriesTimers")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
||||||
{
|
{
|
||||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
|
||||||
await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
@@ -923,7 +914,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="groupId">Group id.</param>
|
/// <param name="groupId">Group id.</param>
|
||||||
/// <returns>A <see cref="NotFoundResult"/>.</returns>
|
/// <returns>A <see cref="NotFoundResult"/>.</returns>
|
||||||
[HttpGet("Recordings/Groups/{groupId}")]
|
[HttpGet("Recordings/Groups/{groupId}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[Obsolete("This endpoint is obsolete.")]
|
[Obsolete("This endpoint is obsolete.")]
|
||||||
public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
|
public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
|
||||||
@@ -937,7 +928,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Guid info returned.</response>
|
/// <response code="200">Guid info returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
|
||||||
[HttpGet("GuideInfo")]
|
[HttpGet("GuideInfo")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<GuideInfo> GetGuideInfo()
|
public ActionResult<GuideInfo> GetGuideInfo()
|
||||||
{
|
{
|
||||||
@@ -951,7 +942,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Created tuner host returned.</response>
|
/// <response code="200">Created tuner host returned.</response>
|
||||||
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
|
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
|
||||||
[HttpPost("TunerHosts")]
|
[HttpPost("TunerHosts")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
|
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
|
||||||
{
|
{
|
||||||
@@ -965,7 +956,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="204">Tuner host deleted.</response>
|
/// <response code="204">Tuner host deleted.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpDelete("TunerHosts")]
|
[HttpDelete("TunerHosts")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult DeleteTunerHost([FromQuery] string? id)
|
public ActionResult DeleteTunerHost([FromQuery] string? id)
|
||||||
{
|
{
|
||||||
@@ -981,7 +972,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Default listings provider info returned.</response>
|
/// <response code="200">Default listings provider info returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
|
||||||
[HttpGet("ListingProviders/Default")]
|
[HttpGet("ListingProviders/Default")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
|
public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
|
||||||
{
|
{
|
||||||
@@ -998,7 +989,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Created listings provider returned.</response>
|
/// <response code="200">Created listings provider returned.</response>
|
||||||
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
|
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
|
||||||
[HttpPost("ListingProviders")]
|
[HttpPost("ListingProviders")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
|
||||||
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
|
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
|
||||||
@@ -1025,7 +1016,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="204">Listing provider deleted.</response>
|
/// <response code="204">Listing provider deleted.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpDelete("ListingProviders")]
|
[HttpDelete("ListingProviders")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult DeleteListingProvider([FromQuery] string? id)
|
public ActionResult DeleteListingProvider([FromQuery] string? id)
|
||||||
{
|
{
|
||||||
@@ -1043,7 +1034,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Available lineups returned.</response>
|
/// <response code="200">Available lineups returned.</response>
|
||||||
/// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
|
/// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
|
||||||
[HttpGet("ListingProviders/Lineups")]
|
[HttpGet("ListingProviders/Lineups")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
|
public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
|
||||||
[FromQuery] string? id,
|
[FromQuery] string? id,
|
||||||
@@ -1060,7 +1051,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Available countries returned.</response>
|
/// <response code="200">Available countries returned.</response>
|
||||||
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
|
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
|
||||||
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
|
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesFile(MediaTypeNames.Application.Json)]
|
[ProducesFile(MediaTypeNames.Application.Json)]
|
||||||
public async Task<ActionResult> GetSchedulesDirectCountries()
|
public async Task<ActionResult> GetSchedulesDirectCountries()
|
||||||
@@ -1081,7 +1072,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Channel mapping options returned.</response>
|
/// <response code="200">Channel mapping options returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
|
||||||
[HttpGet("ChannelMappingOptions")]
|
[HttpGet("ChannelMappingOptions")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
|
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>
|
/// <response code="200">Created channel mapping returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
|
||||||
[HttpPost("ChannelMappings")]
|
[HttpPost("ChannelMappings")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
|
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>
|
/// <response code="200">Tuner host types returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
|
||||||
[HttpGet("TunerHosts/Types")]
|
[HttpGet("TunerHosts/Types")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
|
public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
|
||||||
{
|
{
|
||||||
@@ -1147,7 +1138,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
|
||||||
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
|
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
|
||||||
[HttpGet("Tuners/Discover")]
|
[HttpGet("Tuners/Discover")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
|
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());
|
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
|
||||||
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
|
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>
|
/// <response code="204">Package repositories saved.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Repositories")]
|
[HttpPost("Repositories")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos)
|
public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -98,7 +98,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Limit = limit ?? 0
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -187,7 +188,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
result.AlbumArtist = album.AlbumArtist;
|
result.AlbumArtist = album.AlbumArtist;
|
||||||
break;
|
break;
|
||||||
case Audio song:
|
case Audio song:
|
||||||
result.AlbumArtist = song.AlbumArtists?[0];
|
result.AlbumArtist = song.AlbumArtists?.FirstOrDefault();
|
||||||
result.Artists = song.Artists;
|
result.Artists = song.Artists;
|
||||||
|
|
||||||
MusicAlbum musicAlbum = song.AlbumEntity;
|
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="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="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="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="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="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="limit">Optional. The maximum number of records to return.</param>
|
||||||
@@ -140,6 +145,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? hasImdbId,
|
[FromQuery] bool? hasImdbId,
|
||||||
[FromQuery] bool? hasTmdbId,
|
[FromQuery] bool? hasTmdbId,
|
||||||
[FromQuery] bool? hasTvdbId,
|
[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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||||
[FromQuery] int? startIndex,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
@@ -224,6 +234,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
hasImdbId,
|
hasImdbId,
|
||||||
hasTmdbId,
|
hasTmdbId,
|
||||||
hasTvdbId,
|
hasTvdbId,
|
||||||
|
isMovie,
|
||||||
|
isSeries,
|
||||||
|
isNews,
|
||||||
|
isKids,
|
||||||
|
isSports,
|
||||||
excludeItemIds,
|
excludeItemIds,
|
||||||
startIndex,
|
startIndex,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -117,7 +117,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool enableRedirection = true)
|
[FromQuery] bool enableRedirection = true)
|
||||||
{
|
{
|
||||||
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
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);
|
var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|||||||
@@ -282,16 +282,19 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var success = await _userManager.AuthenticateUser(
|
if (!HttpContext.User.IsInRole(UserRoles.Administrator))
|
||||||
user.Username,
|
|
||||||
request.CurrentPw,
|
|
||||||
request.CurrentPw,
|
|
||||||
HttpContext.GetNormalizedRemoteIp().ToString(),
|
|
||||||
false).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (success == null)
|
|
||||||
{
|
{
|
||||||
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);
|
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
using var state = await StreamingHelpers.GetStreamingState(
|
var state = await StreamingHelpers.GetStreamingState(
|
||||||
streamingRequest,
|
streamingRequest,
|
||||||
Request,
|
Request,
|
||||||
_authContext,
|
_authContext,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ using System.Text;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Models.StreamingDtos;
|
using Jellyfin.Api.Models.StreamingDtos;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
@@ -202,8 +204,18 @@ namespace Jellyfin.Api.Helpers
|
|||||||
|
|
||||||
if (state.VideoStream != null && state.VideoRequest != null)
|
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.
|
// 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.IsNullOrEmpty(state.VideoStream.VideoRange)
|
||||||
&& string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
|
&& string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
|
||||||
&& string.Equals(state.ActualOutputVideoCodec, "hevc", 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");
|
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
|
||||||
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
||||||
|
|
||||||
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0;
|
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
|
||||||
var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
|
var sdrOutputAudioBitrate = 0;
|
||||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
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
|
// Restore the video codec
|
||||||
state.OutputVideoCodec = "copy";
|
state.OutputVideoCodec = "copy";
|
||||||
@@ -250,6 +277,13 @@ namespace Jellyfin.Api.Helpers
|
|||||||
state.VideoStream.Level = originalLevel;
|
state.VideoStream.Level = originalLevel;
|
||||||
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
||||||
builder.Append(newPlaylist);
|
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();
|
return HlsCodecStringHelpers.GetALACString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return HlsCodecStringHelpers.GetOPUSString();
|
||||||
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,7 +745,19 @@ namespace Jellyfin.Api.Helpers
|
|||||||
return oldPlaylist.Replace(
|
return oldPlaylist.Replace(
|
||||||
oldValue.ToString(),
|
oldValue.ToString(),
|
||||||
newValue.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>
|
/// <summary>
|
||||||
/// Codec name for FLAC.
|
/// Codec name for FLAC.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FLAC = "fLaC";
|
public const string FLAC = "flac";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Codec name for ALAC.
|
/// Codec name for ALAC.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ALAC = "alac";
|
public const string ALAC = "alac";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Codec name for OPUS.
|
||||||
|
/// </summary>
|
||||||
|
public const string OPUS = "opus";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a MP3 codec string.
|
/// Gets a MP3 codec string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -101,6 +106,15 @@ namespace Jellyfin.Api.Helpers
|
|||||||
return ALAC;
|
return ALAC;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an OPUS codec string.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>OPUS codec string.</returns>
|
||||||
|
public static string GetOPUSString()
|
||||||
|
{
|
||||||
|
return OPUS;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a H.264 codec string.
|
/// Gets a H.264 codec string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -256,9 +256,17 @@ namespace Jellyfin.Api.Helpers
|
|||||||
streamInfo.StartPositionTicks = startTimeTicks;
|
streamInfo.StartPositionTicks = startTimeTicks;
|
||||||
|
|
||||||
mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
|
mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
|
||||||
|
|
||||||
// Players do not handle this being set according to PlayMethod
|
// 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.SupportsDirectStream =
|
||||||
mediaSource.SupportsTranscoding = streamInfo.PlayMethod == PlayMethod.DirectStream || mediaSource.TranscodingContainer != null;
|
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)
|
if (item is Audio)
|
||||||
{
|
{
|
||||||
@@ -290,7 +298,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream)
|
if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
|
||||||
{
|
{
|
||||||
streamInfo.PlayMethod = PlayMethod.Transcode;
|
streamInfo.PlayMethod = PlayMethod.Transcode;
|
||||||
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
|
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
|
||||||
|
|||||||
@@ -179,15 +179,21 @@ namespace Jellyfin.Api.Helpers
|
|||||||
{
|
{
|
||||||
containerInternal = streamingRequest.Static ?
|
containerInternal = streamingRequest.Static ?
|
||||||
StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
|
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.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);
|
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
|
||||||
|
|
||||||
if (state.VideoRequest != null)
|
if (state.VideoRequest != null)
|
||||||
@@ -235,7 +241,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
|
ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
|
||||||
|
|
||||||
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
||||||
? GetOutputFileExtension(state)
|
? GetOutputFileExtension(state, mediaSource)
|
||||||
: ("." + state.OutputContainer);
|
: ("." + state.OutputContainer);
|
||||||
|
|
||||||
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||||
@@ -312,7 +318,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
|
|
||||||
responseHeaders.Add(
|
responseHeaders.Add(
|
||||||
"contentFeatures.dlna.org",
|
"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.
|
/// Gets the output file extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="state">The state.</param>
|
/// <param name="state">The state.</param>
|
||||||
|
/// <param name="mediaSource">The mediaSource.</param>
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
private static string? GetOutputFileExtension(StreamState state)
|
private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(state.RequestedUrl);
|
var ext = Path.GetExtension(state.RequestedUrl);
|
||||||
|
|
||||||
@@ -425,7 +432,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
var videoCodec = state.Request.VideoCodec;
|
var videoCodec = state.Request.VideoCodec;
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
|
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return ".ts";
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,6 +547,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
state.TargetVideoBitDepth,
|
state.TargetVideoBitDepth,
|
||||||
state.OutputVideoBitrate,
|
state.OutputVideoBitrate,
|
||||||
state.TargetVideoProfile,
|
state.TargetVideoProfile,
|
||||||
|
state.TargetVideoRangeType,
|
||||||
state.TargetVideoLevel,
|
state.TargetVideoLevel,
|
||||||
state.TargetFramerate,
|
state.TargetFramerate,
|
||||||
state.TargetPacketLength,
|
state.TargetPacketLength,
|
||||||
|
|||||||
@@ -654,8 +654,8 @@ namespace Jellyfin.Api.Helpers
|
|||||||
{
|
{
|
||||||
if (EnableThrottling(state))
|
if (EnableThrottling(state))
|
||||||
{
|
{
|
||||||
transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem);
|
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
||||||
state.TranscodingThrottler.Start();
|
transcodingJob.TranscodingThrottler.Start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,18 +663,11 @@ namespace Jellyfin.Api.Helpers
|
|||||||
{
|
{
|
||||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||||
|
|
||||||
// enable throttling when NOT using hardware acceleration
|
return state.InputProtocol == MediaProtocol.File &&
|
||||||
if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
|
state.RunTimeTicks.HasValue &&
|
||||||
{
|
state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
|
||||||
return state.InputProtocol == MediaProtocol.File &&
|
state.IsInputVideo &&
|
||||||
state.RunTimeTicks.HasValue &&
|
state.VideoType == VideoType.VideoFile;
|
||||||
state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
|
|
||||||
state.IsInputVideo &&
|
|
||||||
state.VideoType == VideoType.VideoFile &&
|
|
||||||
!EncodingHelper.IsCopyCodec(state.OutputVideoCodec);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -17,15 +17,16 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
|
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.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" />
|
<ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -17,6 +18,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
|||||||
private readonly ILogger<TranscodingThrottler> _logger;
|
private readonly ILogger<TranscodingThrottler> _logger;
|
||||||
private readonly IConfigurationManager _config;
|
private readonly IConfigurationManager _config;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private Timer? _timer;
|
private Timer? _timer;
|
||||||
private bool _isPaused;
|
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="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param>
|
||||||
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> 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;
|
_job = job;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_config = config;
|
_config = config;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
|
_mediaEncoder = mediaEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -55,7 +59,8 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
|||||||
|
|
||||||
try
|
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;
|
_isPaused = false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -125,11 +130,13 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
|||||||
{
|
{
|
||||||
if (!_isPaused)
|
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
|
try
|
||||||
{
|
{
|
||||||
await _job.Process!.StandardInput.WriteAsync("c").ConfigureAwait(false);
|
await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false);
|
||||||
_isPaused = true;
|
_isPaused = true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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>
|
/// <summary>
|
||||||
/// Gets the video request.
|
/// Gets the video request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -191,11 +186,8 @@ namespace Jellyfin.Api.Models.StreamingDtos
|
|||||||
{
|
{
|
||||||
_mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
|
_mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
TranscodingThrottler?.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TranscodingThrottler = null;
|
|
||||||
TranscodingJob = null;
|
TranscodingJob = null;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ namespace Jellyfin.Data.Entities
|
|||||||
/// <returns><c>True</c> if the user has the specified permission.</returns>
|
/// <returns><c>True</c> if the user has the specified permission.</returns>
|
||||||
public bool HasPermission(PermissionKind kind)
|
public bool HasPermission(PermissionKind kind)
|
||||||
{
|
{
|
||||||
return Permissions.First(p => p.Kind == kind).Value;
|
return Permissions.FirstOrDefault(p => p.Kind == kind)?.Value ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -372,7 +372,15 @@ namespace Jellyfin.Data.Entities
|
|||||||
/// <param name="value">The value to set.</param>
|
/// <param name="value">The value to set.</param>
|
||||||
public void SetPermission(PermissionKind kind, bool value)
|
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>
|
/// <summary>
|
||||||
@@ -382,9 +390,9 @@ namespace Jellyfin.Data.Entities
|
|||||||
/// <returns>A string array containing the user's preferences.</returns>
|
/// <returns>A string array containing the user's preferences.</returns>
|
||||||
public string[] GetPreference(PreferenceKind preference)
|
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>
|
/// <summary>
|
||||||
@@ -395,7 +403,7 @@ namespace Jellyfin.Data.Entities
|
|||||||
/// <returns>A {T} array containing the user's preference.</returns>
|
/// <returns>A {T} array containing the user's preference.</returns>
|
||||||
public T[] GetPreferenceValues<T>(PreferenceKind preference)
|
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))
|
if (string.IsNullOrEmpty(val))
|
||||||
{
|
{
|
||||||
return Array.Empty<T>();
|
return Array.Empty<T>();
|
||||||
@@ -432,8 +440,16 @@ namespace Jellyfin.Data.Entities
|
|||||||
/// <param name="values">The values.</param>
|
/// <param name="values">The values.</param>
|
||||||
public void SetPreference(PreferenceKind preference, string[] values)
|
public void SetPreference(PreferenceKind preference, string[] values)
|
||||||
{
|
{
|
||||||
Preferences.First(p => p.Kind == preference).Value
|
var value = string.Join(Delimiter, values);
|
||||||
= 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>
|
/// <summary>
|
||||||
@@ -444,8 +460,16 @@ namespace Jellyfin.Data.Entities
|
|||||||
/// <typeparam name="T">The type of value.</typeparam>
|
/// <typeparam name="T">The type of value.</typeparam>
|
||||||
public void SetPreference<T>(PreferenceKind preference, T[] values)
|
public void SetPreference<T>(PreferenceKind preference, T[] values)
|
||||||
{
|
{
|
||||||
Preferences.First(p => p.Kind == preference).Value
|
var value = string.Join(Delimiter, values);
|
||||||
= 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>
|
/// <summary>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.8.0</VersionPrefix>
|
<VersionPrefix>10.8.10</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -18,8 +18,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BlurHashSharp" Version="1.2.0" />
|
<PackageReference Include="BlurHashSharp" Version="1.2.0" />
|
||||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
||||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
<PackageReference Include="SkiaSharp" Version="2.88.2" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.3" />
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.2" />
|
||||||
|
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using BlurHashSharp.SkiaSharp;
|
using BlurHashSharp.SkiaSharp;
|
||||||
using Diacritics.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Model.Drawing;
|
using MediaBrowser.Model.Drawing;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using static Jellyfin.Drawing.Skia.SkiaHelper;
|
using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
|
||||||
|
|
||||||
namespace Jellyfin.Drawing.Skia
|
namespace Jellyfin.Drawing.Skia
|
||||||
{
|
{
|
||||||
@@ -19,8 +19,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SkiaEncoder : IImageEncoder
|
public class SkiaEncoder : IImageEncoder
|
||||||
{
|
{
|
||||||
private static readonly HashSet<string> _transparentImageTypes
|
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
||||||
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
|
||||||
|
|
||||||
private readonly ILogger<SkiaEncoder> _logger;
|
private readonly ILogger<SkiaEncoder> _logger;
|
||||||
private readonly IApplicationPaths _appPaths;
|
private readonly IApplicationPaths _appPaths;
|
||||||
@@ -71,7 +70,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
|
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
|
||||||
=> new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
|
=> new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check if the native lib is available.
|
/// Check if the native lib is available.
|
||||||
@@ -109,9 +108,7 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
/// <exception cref="ArgumentNullException">The path is null.</exception>
|
|
||||||
/// <exception cref="FileNotFoundException">The path is not valid.</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)
|
public ImageDimensions GetImageSize(string path)
|
||||||
{
|
{
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
@@ -119,12 +116,27 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
throw new FileNotFoundException("File not found", path);
|
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);
|
using var codec = SKCodec.Create(path, out SKCodecResult result);
|
||||||
EnsureSuccess(result);
|
switch (result)
|
||||||
|
{
|
||||||
var info = codec.Info;
|
case SKCodecResult.Success:
|
||||||
|
var info = codec.Info;
|
||||||
return new ImageDimensions(info.Width, info.Height);
|
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 />
|
/// <inheritdoc />
|
||||||
@@ -138,6 +150,13 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
throw new ArgumentNullException(nameof(path));
|
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
|
// Any larger than 128x128 is too slow and there's no visually discernible difference
|
||||||
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
|
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));
|
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 skiaOutputFormat = GetImageFormat(outputFormat);
|
||||||
|
|
||||||
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
|
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
|
||||||
|
|||||||
@@ -8,19 +8,6 @@ namespace Jellyfin.Drawing.Skia
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SkiaHelper
|
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>
|
/// <summary>
|
||||||
/// Gets the next valid image as a bitmap.
|
/// Gets the next valid image as a bitmap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -463,6 +463,18 @@ namespace Jellyfin.Networking.Manager
|
|||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool IsInLocalNetwork(IPObject address)
|
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)
|
if (address == null)
|
||||||
{
|
{
|
||||||
@@ -481,36 +493,7 @@ namespace Jellyfin.Networking.Manager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// As private addresses can be redefined by Configuration.LocalNetworkAddresses
|
// As private addresses can be redefined by Configuration.LocalNetworkAddresses
|
||||||
return address.IsLoopback() || (_lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address));
|
return IPAddress.IsLoopback(address) || (_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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.3">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</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.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
namespace Jellyfin.Server.Implementations.Migrations
|
namespace Jellyfin.Server.Implementations.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(JellyfinDb))]
|
[DbContext(typeof(JellyfinDb))]
|
||||||
@@ -15,7 +17,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasDefaultSchema("jellyfin")
|
.HasDefaultSchema("jellyfin")
|
||||||
.HasAnnotation("ProductVersion", "5.0.7");
|
.HasAnnotation("ProductVersion", "6.0.9");
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||||
{
|
{
|
||||||
@@ -39,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("AccessSchedules");
|
b.ToTable("AccessSchedules", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
|
||||||
@@ -85,7 +87,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("ActivityLogs");
|
b.HasIndex("DateCreated");
|
||||||
|
|
||||||
|
b.ToTable("ActivityLogs", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
|
||||||
@@ -117,7 +121,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("CustomItemDisplayPreferences");
|
b.ToTable("CustomItemDisplayPreferences", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||||
@@ -174,7 +178,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("UserId", "ItemId", "Client")
|
b.HasIndex("UserId", "ItemId", "Client")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("DisplayPreferences");
|
b.ToTable("DisplayPreferences", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||||
@@ -196,7 +200,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
|
|
||||||
b.HasIndex("DisplayPreferencesId");
|
b.HasIndex("DisplayPreferencesId");
|
||||||
|
|
||||||
b.ToTable("HomeSection");
|
b.ToTable("HomeSection", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||||
@@ -221,7 +225,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("UserId")
|
b.HasIndex("UserId")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("ImageInfos");
|
b.ToTable("ImageInfos", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||||
@@ -265,7 +269,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("ItemDisplayPreferences");
|
b.ToTable("ItemDisplayPreferences", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||||
@@ -296,7 +300,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasFilter("[UserId] IS NOT NULL");
|
.HasFilter("[UserId] IS NOT NULL");
|
||||||
|
|
||||||
b.ToTable("Permissions");
|
b.ToTable("Permissions", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||||
@@ -329,7 +333,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasFilter("[UserId] IS NOT NULL");
|
.HasFilter("[UserId] IS NOT NULL");
|
||||||
|
|
||||||
b.ToTable("Preferences");
|
b.ToTable("Preferences", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
|
||||||
@@ -358,7 +362,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("AccessToken")
|
b.HasIndex("AccessToken")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("ApiKeys");
|
b.ToTable("ApiKeys", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
||||||
@@ -416,7 +420,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId", "DeviceId");
|
b.HasIndex("UserId", "DeviceId");
|
||||||
|
|
||||||
b.ToTable("Devices");
|
b.ToTable("Devices", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
|
||||||
@@ -437,7 +441,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("DeviceId")
|
b.HasIndex("DeviceId")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("DeviceOptions");
|
b.ToTable("DeviceOptions", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||||
@@ -550,7 +554,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||||||
b.HasIndex("Username")
|
b.HasIndex("Username")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Users");
|
b.ToTable("Users", "jellyfin");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
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.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -16,11 +17,16 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
{
|
{
|
||||||
private readonly JellyfinDbProvider _jellyfinDbProvider;
|
private readonly JellyfinDbProvider _jellyfinDbProvider;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IServerApplicationHost _serverApplicationHost;
|
||||||
|
|
||||||
public AuthorizationContext(JellyfinDbProvider jellyfinDb, IUserManager userManager)
|
public AuthorizationContext(
|
||||||
|
JellyfinDbProvider jellyfinDb,
|
||||||
|
IUserManager userManager,
|
||||||
|
IServerApplicationHost serverApplicationHost)
|
||||||
{
|
{
|
||||||
_jellyfinDbProvider = jellyfinDb;
|
_jellyfinDbProvider = jellyfinDb;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
_serverApplicationHost = serverApplicationHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
|
public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
|
||||||
@@ -187,17 +193,17 @@ namespace Jellyfin.Server.Implementations.Security
|
|||||||
authInfo.Token = key.AccessToken;
|
authInfo.Token = key.AccessToken;
|
||||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||||
{
|
{
|
||||||
authInfo.DeviceId = string.Empty;
|
authInfo.DeviceId = _serverApplicationHost.SystemId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||||
{
|
{
|
||||||
authInfo.Device = string.Empty;
|
authInfo.Device = _serverApplicationHost.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||||
{
|
{
|
||||||
authInfo.Version = string.Empty;
|
authInfo.Version = _serverApplicationHost.ApplicationVersionString;
|
||||||
}
|
}
|
||||||
|
|
||||||
authInfo.IsApiKey = true;
|
authInfo.IsApiKey = true;
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy;
|
|||||||
using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy;
|
using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy;
|
||||||
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
||||||
using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
|
using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
|
||||||
|
using Jellyfin.Api.Auth.LiveTvAccessPolicy;
|
||||||
|
using Jellyfin.Api.Auth.LiveTvManagementPolicy;
|
||||||
using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
|
using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
|
||||||
using Jellyfin.Api.Auth.LocalAccessPolicy;
|
using Jellyfin.Api.Auth.LocalAccessPolicy;
|
||||||
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
||||||
@@ -66,6 +68,8 @@ namespace Jellyfin.Server.Extensions
|
|||||||
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
|
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
|
||||||
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
|
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
|
||||||
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
|
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
|
||||||
|
serviceCollection.AddSingleton<IAuthorizationHandler, LiveTvAccessHandler>();
|
||||||
|
serviceCollection.AddSingleton<IAuthorizationHandler, LiveTvManagementHandler>();
|
||||||
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
|
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
|
||||||
return serviceCollection.AddAuthorizationCore(options =>
|
return serviceCollection.AddAuthorizationCore(options =>
|
||||||
{
|
{
|
||||||
@@ -167,6 +171,20 @@ namespace Jellyfin.Server.Extensions
|
|||||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
policy.AddRequirements(new AnonymousLanAccessRequirement());
|
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>(() =>
|
options.MapType<TranscodeReason>(() =>
|
||||||
new OpenApiSchema
|
new OpenApiSchema
|
||||||
{
|
{
|
||||||
Type = "string",
|
Type = "array",
|
||||||
Enum = Enum.GetNames<TranscodeReason>()
|
Items = new OpenApiSchema
|
||||||
.Select(e => new OpenApiString(e))
|
{
|
||||||
.Cast<IOpenApiAny>()
|
Reference = new OpenApiReference
|
||||||
.ToArray()
|
{
|
||||||
|
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.Controller.LiveTv;
|
||||||
using MediaBrowser.Model.ApiClient;
|
using MediaBrowser.Model.ApiClient;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
using MediaBrowser.Model.SyncPlay;
|
using MediaBrowser.Model.SyncPlay;
|
||||||
|
using Microsoft.OpenApi.Any;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
@@ -14,6 +20,19 @@ namespace Jellyfin.Server.Filters
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class AdditionalModelFilter : IDocumentFilter
|
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 />
|
/// <inheritdoc />
|
||||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
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(SessionMessageType), context.SchemaRepository);
|
||||||
context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), 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>
|
||||||
|
|
||||||
<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.EnvironmentVariables" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
<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" Version="6.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.9" />
|
||||||
<PackageReference Include="prometheus-net" Version="6.0.0" />
|
<PackageReference Include="prometheus-net" Version="6.0.0" />
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
|
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.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.Console" Version="4.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.3.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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -19,41 +19,44 @@ namespace Jellyfin.Server.Middleware
|
|||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly ILogger<ResponseTimeMiddleware> _logger;
|
private readonly ILogger<ResponseTimeMiddleware> _logger;
|
||||||
|
|
||||||
private readonly bool _enableWarning;
|
|
||||||
private readonly long _warningThreshold;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
|
/// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next">Next request delegate.</param>
|
/// <param name="next">Next request delegate.</param>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</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(
|
public ResponseTimeMiddleware(
|
||||||
RequestDelegate next,
|
RequestDelegate next,
|
||||||
ILogger<ResponseTimeMiddleware> logger,
|
ILogger<ResponseTimeMiddleware> logger)
|
||||||
IServerConfigurationManager serverConfigurationManager)
|
|
||||||
{
|
{
|
||||||
_next = next;
|
_next = next;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
_enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
|
|
||||||
_warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoke request.
|
/// Invoke request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">Request context.</param>
|
/// <param name="context">Request context.</param>
|
||||||
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
public async Task Invoke(HttpContext context)
|
public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager)
|
||||||
{
|
{
|
||||||
var watch = new Stopwatch();
|
var watch = new Stopwatch();
|
||||||
watch.Start();
|
watch.Start();
|
||||||
|
var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
|
||||||
|
var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
|
||||||
context.Response.OnStarting(() =>
|
context.Response.OnStarting(() =>
|
||||||
{
|
{
|
||||||
watch.Stop();
|
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;
|
var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
|
||||||
context.Response.Headers[ResponseHeaderResponseTime] = responseTimeForCompleteRequest.ToString(CultureInfo.InvariantCulture);
|
context.Response.Headers[ResponseHeaderResponseTime] = responseTimeForCompleteRequest.ToString(CultureInfo.InvariantCulture);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -62,18 +65,5 @@ namespace Jellyfin.Server.Middleware
|
|||||||
// Call the next delegate/middleware in the pipeline
|
// Call the next delegate/middleware in the pipeline
|
||||||
await this._next(context).ConfigureAwait(false);
|
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.Data.Entities.Security;
|
||||||
using Jellyfin.Server.Implementations;
|
using Jellyfin.Server.Implementations;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
using SQLitePCL.pretty;
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
private readonly ILogger<MigrateAuthenticationDb> _logger;
|
private readonly ILogger<MigrateAuthenticationDb> _logger;
|
||||||
private readonly JellyfinDbProvider _dbProvider;
|
private readonly JellyfinDbProvider _dbProvider;
|
||||||
private readonly IServerApplicationPaths _appPaths;
|
private readonly IServerApplicationPaths _appPaths;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MigrateAuthenticationDb"/> class.
|
/// 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="logger">The logger.</param>
|
||||||
/// <param name="dbProvider">The database provider.</param>
|
/// <param name="dbProvider">The database provider.</param>
|
||||||
/// <param name="appPaths">The server application paths.</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;
|
_logger = logger;
|
||||||
_dbProvider = dbProvider;
|
_dbProvider = dbProvider;
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
|
_userManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -74,6 +82,14 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
}
|
}
|
||||||
else
|
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(
|
dbContext.Devices.Add(new Device(
|
||||||
new Guid(row[6].ToString()),
|
new Guid(row[6].ToString()),
|
||||||
row[3].ToString(),
|
row[3].ToString(),
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ namespace Jellyfin.Server
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appHost.Dispose();
|
await appHost.DisposeAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_restartOnShutdown)
|
if (_restartOnShutdown)
|
||||||
@@ -545,12 +545,14 @@ namespace Jellyfin.Server
|
|||||||
const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
|
const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
|
||||||
Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
|
Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
|
||||||
?? throw new InvalidOperationException($"Invalid resource path: '{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 (resource.ConfigureAwait(false))
|
||||||
await using (dst.ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
// Copy the resource contents to the expected file path for the config file
|
Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||||
await resource.CopyToAsync(dst).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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ namespace MediaBrowser.Common.Configuration
|
|||||||
/// <returns>System.Object.</returns>
|
/// <returns>System.Object.</returns>
|
||||||
object GetConfiguration(string key);
|
object GetConfiguration(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the array of coniguration stores.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Array of ConfigurationStore.</returns>
|
||||||
|
ConfigurationStore[] GetConfigurationStores();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the type of the configuration.
|
/// Gets the type of the configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Common</PackageId>
|
<PackageId>Jellyfin.Common</PackageId>
|
||||||
<VersionPrefix>10.8.0</VersionPrefix>
|
<VersionPrefix>10.8.10</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ namespace MediaBrowser.Common.Net
|
|||||||
address = address.MapToIPv4();
|
address = address.MapToIPv4();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsLoopback(address))
|
if (IPAddress.IsLoopback(address))
|
||||||
{
|
{
|
||||||
return (address, prefixLength);
|
return (address, prefixLength);
|
||||||
}
|
}
|
||||||
@@ -102,31 +102,6 @@ namespace MediaBrowser.Common.Net
|
|||||||
return (new IPAddress(addressBytes), prefixLength);
|
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>
|
/// <summary>
|
||||||
/// Tests to see if the ip address is an IP6 address.
|
/// Tests to see if the ip address is an IP6 address.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -295,7 +270,7 @@ namespace MediaBrowser.Common.Net
|
|||||||
/// <returns>True if it is.</returns>
|
/// <returns>True if it is.</returns>
|
||||||
public virtual bool IsLoopback()
|
public virtual bool IsLoopback()
|
||||||
{
|
{
|
||||||
return IsLoopback(Address);
|
return IPAddress.IsLoopback(Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ namespace MediaBrowser.Controller.ClientEvent
|
|||||||
{
|
{
|
||||||
var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log";
|
var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log";
|
||||||
var logFilePath = Path.Combine(_applicationPaths.LogDirectoryPath, fileName);
|
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 using var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
||||||
await fileContents.CopyToAsync(fileStream).ConfigureAwait(false);
|
await fileContents.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
return fileName;
|
return fileName;
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ namespace MediaBrowser.Controller.Drawing
|
|||||||
/// <returns>BlurHash.</returns>
|
/// <returns>BlurHash.</returns>
|
||||||
string GetImageBlurHash(string path);
|
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>
|
/// <summary>
|
||||||
/// Gets the image cache tag.
|
/// Gets the image cache tag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -169,8 +169,8 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||||||
|
|
||||||
var childUpdateType = ItemUpdateType.None;
|
var childUpdateType = ItemUpdateType.None;
|
||||||
|
|
||||||
// Refresh songs
|
// Refresh songs only and not m3u files in album folder
|
||||||
foreach (var item in items)
|
foreach (var item in items.OfType<Audio>())
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ using System.Linq;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Diacritics.Extensions;
|
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Diacritics.Extensions;
|
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Entities.Audio
|
namespace MediaBrowser.Controller.Entities.Audio
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ using System.Text;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Diacritics.Extensions;
|
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
@@ -1864,7 +1863,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
data.PlaybackPositionTicks = 0;
|
data.PlaybackPositionTicks = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
|
data.LastPlayedDate = datePlayed ?? DateTime.UtcNow;
|
||||||
data.Played = true;
|
data.Played = true;
|
||||||
|
|
||||||
UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
|
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)
|
private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
|
||||||
{
|
{
|
||||||
var ids = query.ItemIds;
|
return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
|
public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Diacritics.Extensions;
|
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Entities
|
namespace MediaBrowser.Controller.Entities
|
||||||
|
|||||||
@@ -205,6 +205,16 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
public int? MinIndexNumber { get; set; }
|
public int? MinIndexNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum ParentIndexNumber and IndexNumber.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// It produces this where clause:
|
||||||
|
/// <para>(ParentIndexNumber = X and IndexNumber >= Y) or ParentIndexNumber > X.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public (int ParentIndexNumber, int IndexNumber)? MinParentAndIndexNumber { get; set; }
|
||||||
|
|
||||||
public int? AiredDuringSeason { get; set; }
|
public int? AiredDuringSeason { get; set; }
|
||||||
|
|
||||||
public double? MinCriticRating { get; set; }
|
public double? MinCriticRating { get; set; }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Diacritics.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Diacritics.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Entities
|
namespace MediaBrowser.Controller.Entities
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
DtoOptions = options
|
DtoOptions = options
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!user.DisplayMissingEpisodes)
|
if (user == null || !user.DisplayMissingEpisodes)
|
||||||
{
|
{
|
||||||
query.IsMissing = false;
|
query.IsMissing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user