mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-16 08:08:16 +00:00
Compare commits
347 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e93d03d8cb | ||
|
|
a656799dc8 | ||
|
|
83d2c69516 | ||
|
|
204fdeb035 | ||
|
|
c4cdcb73fc | ||
|
|
1e0bd32358 | ||
|
|
bf5f00a383 | ||
|
|
0430ffecb6 | ||
|
|
85cfd080f1 | ||
|
|
a173d01139 | ||
|
|
3e7fad55de | ||
|
|
8cd685a4e9 | ||
|
|
9d565bbb83 | ||
|
|
ab855af95e | ||
|
|
0bac1eab98 | ||
|
|
97c2ba0115 | ||
|
|
4df1003029 | ||
|
|
3269ce56ca | ||
|
|
ce8eddd484 | ||
|
|
5e2872509a | ||
|
|
d3f4dcf6f6 | ||
|
|
be5e10ac37 | ||
|
|
b85a0288a7 | ||
|
|
f8fd851961 | ||
|
|
757f88b1a2 | ||
|
|
fa732bf4a1 | ||
|
|
4f6edd9c3c | ||
|
|
768497d0ff | ||
|
|
f1dc7d3a66 | ||
|
|
a732a28229 | ||
|
|
17626b8e48 | ||
|
|
98c6c34fbb | ||
|
|
bec8d7b3f5 | ||
|
|
2acae258b8 | ||
|
|
643df48707 | ||
|
|
2b98ce052e | ||
|
|
702347df50 | ||
|
|
9e5aa3e87e | ||
|
|
2cd29d1cfd | ||
|
|
eba95cc7f0 | ||
|
|
82ad2633fd | ||
|
|
d9f5619c9a | ||
|
|
d5a8419bc5 | ||
|
|
c448a4f6a5 | ||
|
|
faac37bcf9 | ||
|
|
5921379a29 | ||
|
|
79bb7560dc | ||
|
|
bf37db7f42 | ||
|
|
0ad70bb699 | ||
|
|
e6313d01eb | ||
|
|
876a6b9aec | ||
|
|
e0344353cd | ||
|
|
9799136daf | ||
|
|
3a5503be5f | ||
|
|
2cc0869144 | ||
|
|
3d735e242a | ||
|
|
31712e5da9 | ||
|
|
060097703b | ||
|
|
233e079e58 | ||
|
|
eafd785eb6 | ||
|
|
9908dad045 | ||
|
|
2b4bf81575 | ||
|
|
0c7ceb1545 | ||
|
|
173a963dbf | ||
|
|
6821a2ab35 | ||
|
|
efc79295de | ||
|
|
4d1a583297 | ||
|
|
c94a99fced | ||
|
|
5fdea32dca | ||
|
|
d1c668e230 | ||
|
|
6d662b6587 | ||
|
|
22a8283a9e | ||
|
|
0d9d2e0690 | ||
|
|
edaba7dbe5 | ||
|
|
e8b0ae07af | ||
|
|
c807712246 | ||
|
|
9a14a624a8 | ||
|
|
037eeed746 | ||
|
|
8ecb9558e2 | ||
|
|
8d04c98e35 | ||
|
|
09f1c7f535 | ||
|
|
0ac18a50f5 | ||
|
|
4ebae248df | ||
|
|
fbb9acf58b | ||
|
|
87f081c8ac | ||
|
|
44077b4f5c | ||
|
|
060a80bef7 | ||
|
|
885a1b02c1 | ||
|
|
1dea309ae4 | ||
|
|
32227c76b7 | ||
|
|
a7c43643a4 | ||
|
|
2a5efeb3bb | ||
|
|
464136cfc9 | ||
|
|
31673cc27d | ||
|
|
6a909f956e | ||
|
|
20e9db8308 | ||
|
|
f8b8fdace6 | ||
|
|
2a6e292153 | ||
|
|
c9f3d9bdde | ||
|
|
8d49e0099c | ||
|
|
76e3da6a40 | ||
|
|
f0faddcc44 | ||
|
|
e6606d41ce | ||
|
|
7c8aea7859 | ||
|
|
c4c5af40a1 | ||
|
|
383d514353 | ||
|
|
6fc8237242 | ||
|
|
79d7a4d4df | ||
|
|
e90031b4cc | ||
|
|
4f3d562d75 | ||
|
|
6c8b40f413 | ||
|
|
ec81dc9be2 | ||
|
|
45f3fb1cfc | ||
|
|
f83a24ec43 | ||
|
|
84c03a2d93 | ||
|
|
bc8e249080 | ||
|
|
5ea9a74289 | ||
|
|
987d31ea16 | ||
|
|
f850779781 | ||
|
|
3bdc2bff5f | ||
|
|
5c6a84549a | ||
|
|
48da35f91f | ||
|
|
39b29eb9f1 | ||
|
|
a6740bf51e | ||
|
|
c7797d3ead | ||
|
|
c86d5838be | ||
|
|
43223b9036 | ||
|
|
c71385d2db | ||
|
|
ad5becc524 | ||
|
|
7c75dcfb9c | ||
|
|
7937e31a9b | ||
|
|
1f1f26306b | ||
|
|
577399ca05 | ||
|
|
e7ea7c0383 | ||
|
|
9fe7751d05 | ||
|
|
bf129ab9b8 | ||
|
|
6d23de64c0 | ||
|
|
e4f48bb486 | ||
|
|
866b4460b1 | ||
|
|
9db0b275ff | ||
|
|
14008fd7d0 | ||
|
|
8532d88a71 | ||
|
|
737c739d33 | ||
|
|
679e83082f | ||
|
|
d8e53f35a5 | ||
|
|
7a0e7b3cf8 | ||
|
|
373c63bcc7 | ||
|
|
be5d343efb | ||
|
|
c9c91cc34e | ||
|
|
774b4a0d3f | ||
|
|
a26cded0f5 | ||
|
|
4ec82ec662 | ||
|
|
879787212e | ||
|
|
23100c9b86 | ||
|
|
e6124bc154 | ||
|
|
8753b7200f | ||
|
|
88d5230bab | ||
|
|
2920c52d61 | ||
|
|
de196a7687 | ||
|
|
ba026716c1 | ||
|
|
125ee88311 | ||
|
|
c53f6a2890 | ||
|
|
649b4c49e0 | ||
|
|
848ea703bc | ||
|
|
0adadff3e7 | ||
|
|
ffdc3a6734 | ||
|
|
a51cd4f8db | ||
|
|
af87706379 | ||
|
|
8422ab687b | ||
|
|
64753cfc7f | ||
|
|
632fb05f46 | ||
|
|
527ed0607d | ||
|
|
b59daab273 | ||
|
|
8f28d52929 | ||
|
|
749b263c48 | ||
|
|
80c68b8948 | ||
|
|
a5687793c9 | ||
|
|
b344771f8a | ||
|
|
3ff78b687d | ||
|
|
d260f30810 | ||
|
|
7ffdde9a0b | ||
|
|
e14194bfe2 | ||
|
|
3bf1a7e445 | ||
|
|
1faee43b11 | ||
|
|
31f9938e3a | ||
|
|
ae9fd4ab35 | ||
|
|
71ed7f7676 | ||
|
|
3b6e003029 | ||
|
|
9357d610b1 | ||
|
|
1d4755894e | ||
|
|
2320f06666 | ||
|
|
8296f07a39 | ||
|
|
30f6263806 | ||
|
|
a9249393e1 | ||
|
|
f49a051a5f | ||
|
|
5bcab0f0f8 | ||
|
|
c5a2ff8ac4 | ||
|
|
494ed7e4d2 | ||
|
|
dd97e6bc45 | ||
|
|
7323ccfc23 | ||
|
|
d258a87fda | ||
|
|
77a007a24d | ||
|
|
a380153f92 | ||
|
|
56c81696d3 | ||
|
|
7297431f23 | ||
|
|
f2c7bccb89 | ||
|
|
b0b4068ddf | ||
|
|
3bd2cc9860 | ||
|
|
feb035b9e0 | ||
|
|
82f362abd9 | ||
|
|
04b73cace6 | ||
|
|
3b69f38a1f | ||
|
|
126da94020 | ||
|
|
f9dffa767f | ||
|
|
444b0ea310 | ||
|
|
484427b4aa | ||
|
|
c3f0649fde | ||
|
|
e877486056 | ||
|
|
9854751137 | ||
|
|
057e8ef240 | ||
|
|
205783f46f | ||
|
|
b2fb96ffed | ||
|
|
ee22feb89a | ||
|
|
ca5979cd77 | ||
|
|
d36f49589a | ||
|
|
70f37f0527 | ||
|
|
dfe0aef530 | ||
|
|
9e31d5a73f | ||
|
|
f088ca5555 | ||
|
|
2b46917dcf | ||
|
|
7bae6eff95 | ||
|
|
d0fd23bb4b | ||
|
|
d694a6c09a | ||
|
|
58f61ed118 | ||
|
|
b9da0e7f83 | ||
|
|
7eaa0600e0 | ||
|
|
47c2c536e4 | ||
|
|
7ef9e95d75 | ||
|
|
f8ea4577ab | ||
|
|
72da42cb0a | ||
|
|
dbfa0f3027 | ||
|
|
78f437401b | ||
|
|
1db748399c | ||
|
|
a41c67d16b | ||
|
|
84a1674f39 | ||
|
|
81e535fc62 | ||
|
|
f9d26ea1bc | ||
|
|
5f3dbd8294 | ||
|
|
9cebdfdec0 | ||
|
|
891ccd7bb2 | ||
|
|
54778d875d | ||
|
|
39d185c7b1 | ||
|
|
a7d45b5d3a | ||
|
|
7efa4e38c1 | ||
|
|
506ed6940b | ||
|
|
5dbe16d3e6 | ||
|
|
4c178e9188 | ||
|
|
50bc41d84d | ||
|
|
e931f5a32b | ||
|
|
d2caed25fb | ||
|
|
3f37ef70e1 | ||
|
|
d342b79218 | ||
|
|
c35fc382d4 | ||
|
|
cb6e6879e2 | ||
|
|
a71b190142 | ||
|
|
910df89cce | ||
|
|
5f15339919 | ||
|
|
56e7b323de | ||
|
|
a3a751a4f5 | ||
|
|
c85255a615 | ||
|
|
8ea8dcf128 | ||
|
|
7884e7e829 | ||
|
|
3478554249 | ||
|
|
9898c10880 | ||
|
|
ec2ad4ec8c | ||
|
|
1ffc77b43d | ||
|
|
ae79bbc34c | ||
|
|
52704e8dd0 | ||
|
|
294ab0757e | ||
|
|
bdd52df230 | ||
|
|
73117b079c | ||
|
|
b60905f991 | ||
|
|
6d5c697183 | ||
|
|
24c56328f2 | ||
|
|
ef037ad371 | ||
|
|
a64e21f57a | ||
|
|
56e135f5e6 | ||
|
|
ae22d0b7a5 | ||
|
|
b36543275f | ||
|
|
2c0c3eb3ee | ||
|
|
f1d56aa5ce | ||
|
|
0b6fbebf72 | ||
|
|
db714f967e | ||
|
|
f020bd6f3b | ||
|
|
3275f83c3b | ||
|
|
f7813803c2 | ||
|
|
477b922e4a | ||
|
|
be72001ff9 | ||
|
|
6749313249 | ||
|
|
4ebe70cf6a | ||
|
|
c4051ac16d | ||
|
|
3491f0968b | ||
|
|
fd4ffc6ba3 | ||
|
|
5912a49d1d | ||
|
|
f336647d57 | ||
|
|
9b805c9e83 | ||
|
|
19ccf414ac | ||
|
|
0504ed9fe6 | ||
|
|
c243f588a0 | ||
|
|
46491d0813 | ||
|
|
c7c0cdad95 | ||
|
|
255f5a6707 | ||
|
|
3f497459cb | ||
|
|
5d66c84f2d | ||
|
|
052a59ac3e | ||
|
|
1b8a251991 | ||
|
|
1a787e273a | ||
|
|
16fba6035c | ||
|
|
d73e9f3af5 | ||
|
|
2888080098 | ||
|
|
c8282e8441 | ||
|
|
b295b0478c | ||
|
|
42aaea3556 | ||
|
|
07b39655eb | ||
|
|
0f75f17736 | ||
|
|
83d8dbf93e | ||
|
|
e5aa708cb9 | ||
|
|
ac760b9c86 | ||
|
|
c07c7b753c | ||
|
|
8b69b0f521 | ||
|
|
079fac4a54 | ||
|
|
21afec3225 | ||
|
|
11c5a0b182 | ||
|
|
f318417c4f | ||
|
|
874fcaba69 | ||
|
|
5204863705 | ||
|
|
0f7ba42987 | ||
|
|
3e8fe1ce11 | ||
|
|
044ff0542b | ||
|
|
abfbd04782 | ||
|
|
8d0024ec49 | ||
|
|
8f761a64f5 | ||
|
|
a64eebe79f | ||
|
|
8bb4cd017c | ||
|
|
c5f09ab650 | ||
|
|
99df9c8fcd | ||
|
|
4b563f4d7e |
@@ -27,6 +27,7 @@
|
||||
- [cvium](https://github.com/cvium)
|
||||
- [dannymichel](https://github.com/dannymichel)
|
||||
- [DaveChild](https://github.com/DaveChild)
|
||||
- [DavidFair](https://github.com/DavidFair)
|
||||
- [Delgan](https://github.com/Delgan)
|
||||
- [dcrdev](https://github.com/dcrdev)
|
||||
- [dhartung](https://github.com/dhartung)
|
||||
@@ -36,6 +37,7 @@
|
||||
- [dmitrylyzo](https://github.com/dmitrylyzo)
|
||||
- [DMouse10462](https://github.com/DMouse10462)
|
||||
- [DrPandemic](https://github.com/DrPandemic)
|
||||
- [eglia](https://github.com/eglia)
|
||||
- [EraYaN](https://github.com/EraYaN)
|
||||
- [escabe](https://github.com/escabe)
|
||||
- [excelite](https://github.com/excelite)
|
||||
@@ -157,6 +159,9 @@
|
||||
- [jonas-resch](https://github.com/jonas-resch)
|
||||
- [vgambier](https://github.com/vgambier)
|
||||
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
|
||||
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
||||
- [TheTyrius](https://github.com/TheTyrius)
|
||||
- [Çağrı Sakaoğlu](https://github.com/ilovepilav)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
@@ -225,3 +230,6 @@
|
||||
- [gnuyent](https://github.com/gnuyent)
|
||||
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
||||
- [Jakob Kukla](https://github.com/jakobkukla)
|
||||
- [Utku Özdemir](https://github.com/utkuozdemir)
|
||||
- [JPUC1143](https://github.com/Jpuc1143/)
|
||||
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
||||
|
||||
@@ -72,7 +72,7 @@ COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# because of changes in docker and systemd we need to not build in parallel at the moment
|
||||
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
|
||||
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
|
||||
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
# Discard objs - may cause failures if exists
|
||||
RUN find . -type d -name obj | xargs -r rm -r
|
||||
# Build
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
|
||||
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
FROM app
|
||||
|
||||
|
||||
@@ -221,6 +221,7 @@ namespace Emby.Dlna.Didl
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
@@ -376,6 +377,7 @@ namespace Emby.Dlna.Didl
|
||||
targetHeight,
|
||||
streamInfo.TargetVideoBitDepth,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
|
||||
@@ -313,7 +313,7 @@ namespace Emby.Dlna.Main
|
||||
|
||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
|
||||
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(false) + descriptorUri);
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri);
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
{
|
||||
|
||||
@@ -561,6 +561,7 @@ namespace Emby.Dlna.PlayTo
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoRangeType,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
|
||||
@@ -395,7 +395,13 @@ namespace Emby.Drawing
|
||||
public string GetImageBlurHash(string path)
|
||||
{
|
||||
var size = GetImageDimensions(path);
|
||||
if (size.Width <= 0 || size.Height <= 0)
|
||||
return GetImageBlurHash(path, size);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetImageBlurHash(string path, ImageDimensions imageDimensions)
|
||||
{
|
||||
if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
@@ -403,8 +409,8 @@ namespace Emby.Drawing
|
||||
// We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance.
|
||||
// One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width.
|
||||
// See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components
|
||||
float xCompF = MathF.Sqrt(16.0f * size.Width / size.Height);
|
||||
float yCompF = xCompF * size.Height / size.Width;
|
||||
float xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height);
|
||||
float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width;
|
||||
|
||||
int xComp = Math.Min((int)xCompF + 1, 9);
|
||||
int yComp = Math.Min((int)yCompF + 1, 9);
|
||||
@@ -439,47 +445,46 @@ namespace Emby.Drawing
|
||||
.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private async Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
|
||||
private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
|
||||
{
|
||||
var inputFormat = Path.GetExtension(originalImagePath)
|
||||
.TrimStart('.')
|
||||
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
|
||||
var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString();
|
||||
|
||||
// These are just jpg files renamed as tbn
|
||||
if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (originalImagePath, dateModified);
|
||||
return Task.FromResult((originalImagePath, dateModified));
|
||||
}
|
||||
|
||||
if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
||||
{
|
||||
try
|
||||
{
|
||||
string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
// TODO _mediaEncoder.ConvertImage is not implemented
|
||||
// if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
//
|
||||
// string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
|
||||
// var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
|
||||
//
|
||||
// var file = _fileSystem.GetFileInfo(outputPath);
|
||||
// if (!file.Exists)
|
||||
// {
|
||||
// await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
|
||||
// dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// dateModified = file.LastWriteTimeUtc;
|
||||
// }
|
||||
//
|
||||
// originalImagePath = outputPath;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
|
||||
// }
|
||||
// }
|
||||
|
||||
string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
|
||||
var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
|
||||
|
||||
var file = _fileSystem.GetFileInfo(outputPath);
|
||||
if (!file.Exists)
|
||||
{
|
||||
await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
|
||||
dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
dateModified = file.LastWriteTimeUtc;
|
||||
}
|
||||
|
||||
originalImagePath = outputPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
|
||||
}
|
||||
}
|
||||
|
||||
return (originalImagePath, dateModified);
|
||||
return Task.FromResult((originalImagePath, dateModified));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -250,7 +250,6 @@ namespace Emby.Naming.Common
|
||||
".sfx",
|
||||
".shn",
|
||||
".sid",
|
||||
".spc",
|
||||
".stm",
|
||||
".strm",
|
||||
".ult",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.8.0</VersionPrefix>
|
||||
<VersionPrefix>10.8.13</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="TagLibSharp" Version="2.2.0" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// Class CompositionRoot.
|
||||
/// </summary>
|
||||
public abstract class ApplicationHost : IServerApplicationHost, IDisposable
|
||||
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The environment variable prefixes to log at server startup.
|
||||
@@ -1088,15 +1088,7 @@ namespace Emby.Server.Implementations
|
||||
return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
|
||||
}
|
||||
|
||||
// Published server ends with a /
|
||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||
{
|
||||
// Published server ends with a '/', so we need to remove it.
|
||||
return PublishedServerUrl.Trim('/');
|
||||
}
|
||||
|
||||
string smart = NetManager.GetBindInterface(request, out var port);
|
||||
return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
|
||||
return GetSmartApiUrl(request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -1114,13 +1106,13 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetApiUrlForLocalAccess(bool allowHttps = true)
|
||||
public string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true)
|
||||
{
|
||||
// With an empty source, the port will be null
|
||||
string smart = NetManager.GetBindInterface(string.Empty, out _);
|
||||
var smart = NetManager.GetBindInterface(hostname ?? IPHost.None, out _);
|
||||
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
||||
int? port = !allowHttps ? HttpPort : null;
|
||||
return GetLocalApiUrl(smart.Trim('/'), scheme, port);
|
||||
return GetLocalApiUrl(smart, scheme, port);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -1134,11 +1126,13 @@ namespace Emby.Server.Implementations
|
||||
|
||||
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
||||
// not. For consistency, always trim the trailing slash.
|
||||
scheme ??= ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
|
||||
var isHttps = string.Equals(scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
|
||||
return new UriBuilder
|
||||
{
|
||||
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
||||
Scheme = scheme,
|
||||
Host = hostname,
|
||||
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
||||
Port = port ?? (isHttps ? HttpsPort : HttpPort),
|
||||
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||
}.ToString().TrimEnd('/');
|
||||
}
|
||||
@@ -1230,5 +1224,49 @@ namespace Emby.Server.Implementations
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await DisposeAsyncCore().ConfigureAwait(false);
|
||||
Dispose(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
|
||||
/// </summary>
|
||||
/// <returns>A ValueTask.</returns>
|
||||
protected virtual async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
var type = GetType();
|
||||
|
||||
Logger.LogInformation("Disposing {Type}", type.Name);
|
||||
|
||||
foreach (var (part, _) in _disposableParts)
|
||||
{
|
||||
var partType = part.GetType();
|
||||
if (partType == type)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Disposing {Type}", partType.Name);
|
||||
|
||||
try
|
||||
{
|
||||
part.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error disposing {Type}", partType.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// used for closing websockets
|
||||
foreach (var session in _sessionManager.Sessions)
|
||||
{
|
||||
await session.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,15 @@ namespace Emby.Server.Implementations.Data
|
||||
"CodecTimeBase",
|
||||
"ColorPrimaries",
|
||||
"ColorSpace",
|
||||
"ColorTransfer"
|
||||
"ColorTransfer",
|
||||
"DvVersionMajor",
|
||||
"DvVersionMinor",
|
||||
"DvProfile",
|
||||
"DvLevel",
|
||||
"RpuPresentFlag",
|
||||
"ElPresentFlag",
|
||||
"BlPresentFlag",
|
||||
"DvBlSignalCompatibilityId"
|
||||
};
|
||||
|
||||
private static readonly string _mediaStreamSaveColumnsInsertQuery =
|
||||
@@ -341,7 +349,7 @@ namespace Emby.Server.Implementations.Data
|
||||
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
|
||||
{
|
||||
const string CreateMediaStreamsTableCommand
|
||||
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))";
|
||||
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, PRIMARY KEY (ItemId, StreamIndex))";
|
||||
const string CreateMediaAttachmentsTableCommand
|
||||
= "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
|
||||
|
||||
@@ -555,6 +563,15 @@ namespace Emby.Server.Implementations.Data
|
||||
AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
|
||||
|
||||
AddColumn(db, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "DvProfile", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "DvLevel", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
|
||||
AddColumn(db, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
|
||||
},
|
||||
TransactionMode);
|
||||
|
||||
@@ -2403,7 +2420,7 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
|
||||
// genres, tags, studios, person, year?
|
||||
builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId))");
|
||||
builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))");
|
||||
|
||||
if (item is MusicArtist)
|
||||
{
|
||||
@@ -3058,12 +3075,12 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "(select CleanValue from itemvalues where ItemId=Guid and Type=0 LIMIT 1)";
|
||||
return "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)";
|
||||
}
|
||||
|
||||
if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "(select CleanValue from itemvalues where ItemId=Guid and Type=1 LIMIT 1)";
|
||||
return "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)";
|
||||
}
|
||||
|
||||
if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -3073,7 +3090,7 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "(select CleanValue from itemvalues where ItemId=Guid and Type=3 LIMIT 1)";
|
||||
return "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)";
|
||||
}
|
||||
|
||||
if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -3146,6 +3163,11 @@ namespace Emby.Server.Implementations.Data
|
||||
return ItemSortBy.IndexNumber;
|
||||
}
|
||||
|
||||
if (string.Equals(name, ItemSortBy.SimilarityScore, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ItemSortBy.SimilarityScore;
|
||||
}
|
||||
|
||||
// Unknown SortBy, just sort by the SortName.
|
||||
return ItemSortBy.SortName;
|
||||
}
|
||||
@@ -3519,6 +3541,13 @@ namespace Emby.Server.Implementations.Data
|
||||
statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
|
||||
}
|
||||
|
||||
if (query.MinParentAndIndexNumber.HasValue)
|
||||
{
|
||||
whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)");
|
||||
statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber);
|
||||
statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber);
|
||||
}
|
||||
|
||||
if (query.MinDateCreated.HasValue)
|
||||
{
|
||||
whereClauses.Add("DateCreated>=@MinDateCreated");
|
||||
@@ -3846,7 +3875,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@ArtistIds" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
@@ -3867,7 +3896,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@ArtistIds" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
@@ -3888,7 +3917,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@ArtistIds" + index;
|
||||
|
||||
clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type=1))");
|
||||
clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
@@ -3930,7 +3959,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@ExcludeArtistId" + index;
|
||||
|
||||
clauses.Add("(guid not in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||
clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind(paramName, artistId);
|
||||
@@ -3951,7 +3980,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@GenreId" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind(paramName, genreId);
|
||||
@@ -3970,7 +3999,7 @@ namespace Emby.Server.Implementations.Data
|
||||
var index = 0;
|
||||
foreach (var item in query.Genres)
|
||||
{
|
||||
clauses.Add("@Genre" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=2)");
|
||||
clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@Genre" + index, GetCleanValue(item));
|
||||
@@ -3989,7 +4018,7 @@ namespace Emby.Server.Implementations.Data
|
||||
var index = 0;
|
||||
foreach (var item in tags)
|
||||
{
|
||||
clauses.Add("@Tag" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=4)");
|
||||
clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@Tag" + index, GetCleanValue(item));
|
||||
@@ -4008,7 +4037,7 @@ namespace Emby.Server.Implementations.Data
|
||||
var index = 0;
|
||||
foreach (var item in excludeTags)
|
||||
{
|
||||
clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from itemvalues where ItemId=Guid and Type=4)");
|
||||
clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@ExcludeTag" + index, GetCleanValue(item));
|
||||
@@ -4029,7 +4058,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var paramName = "@StudioId" + index;
|
||||
|
||||
clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
|
||||
clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
|
||||
|
||||
if (statement != null)
|
||||
{
|
||||
@@ -4508,7 +4537,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
int index = 0;
|
||||
string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++));
|
||||
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
||||
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -4743,11 +4772,11 @@ namespace Emby.Server.Implementations.Data
|
||||
';',
|
||||
new string[]
|
||||
{
|
||||
"delete from itemvalues where type = 6",
|
||||
"delete from ItemValues where type = 6",
|
||||
|
||||
"insert into itemvalues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4",
|
||||
"insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4",
|
||||
|
||||
@"insert into itemvalues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
|
||||
@"insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
|
||||
FROM AncestorIds
|
||||
LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
|
||||
where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 "
|
||||
@@ -4912,6 +4941,7 @@ SELECT key FROM UserDatas WHERE isFavorite=@IsFavorite AND userId=@UserId)
|
||||
AND Type = @InternalPersonType)");
|
||||
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
|
||||
statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
|
||||
statement?.TryBind("@UserId", query.User.InternalId);
|
||||
}
|
||||
|
||||
if (!query.ItemId.Equals(default))
|
||||
@@ -4966,11 +4996,6 @@ AND Type = @InternalPersonType)");
|
||||
statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
|
||||
}
|
||||
|
||||
if (query.User != null)
|
||||
{
|
||||
statement?.TryBind("@UserId", query.User.InternalId);
|
||||
}
|
||||
|
||||
return whereClauses;
|
||||
}
|
||||
|
||||
@@ -5854,6 +5879,15 @@ AND Type = @InternalPersonType)");
|
||||
statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries);
|
||||
statement.TryBind("@ColorSpace" + index, stream.ColorSpace);
|
||||
statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer);
|
||||
|
||||
statement.TryBind("@DvVersionMajor" + index, stream.DvVersionMajor);
|
||||
statement.TryBind("@DvVersionMinor" + index, stream.DvVersionMinor);
|
||||
statement.TryBind("@DvProfile" + index, stream.DvProfile);
|
||||
statement.TryBind("@DvLevel" + index, stream.DvLevel);
|
||||
statement.TryBind("@RpuPresentFlag" + index, stream.RpuPresentFlag);
|
||||
statement.TryBind("@ElPresentFlag" + index, stream.ElPresentFlag);
|
||||
statement.TryBind("@BlPresentFlag" + index, stream.BlPresentFlag);
|
||||
statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId);
|
||||
}
|
||||
|
||||
statement.Reset();
|
||||
@@ -6025,6 +6059,46 @@ AND Type = @InternalPersonType)");
|
||||
item.ColorTransfer = colorTransfer;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(35, out var dvVersionMajor))
|
||||
{
|
||||
item.DvVersionMajor = dvVersionMajor;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(36, out var dvVersionMinor))
|
||||
{
|
||||
item.DvVersionMinor = dvVersionMinor;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(37, out var dvProfile))
|
||||
{
|
||||
item.DvProfile = dvProfile;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(38, out var dvLevel))
|
||||
{
|
||||
item.DvLevel = dvLevel;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(39, out var rpuPresentFlag))
|
||||
{
|
||||
item.RpuPresentFlag = rpuPresentFlag;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(40, out var elPresentFlag))
|
||||
{
|
||||
item.ElPresentFlag = elPresentFlag;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(41, out var blPresentFlag))
|
||||
{
|
||||
item.BlPresentFlag = blPresentFlag;
|
||||
}
|
||||
|
||||
if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
|
||||
{
|
||||
item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
|
||||
}
|
||||
|
||||
if (item.Type == MediaStreamType.Subtitle)
|
||||
{
|
||||
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
|
||||
|
||||
@@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.People))
|
||||
{
|
||||
AttachPeople(dto, item);
|
||||
AttachPeople(dto, item, user);
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
|
||||
@@ -503,7 +503,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
/// </summary>
|
||||
/// <param name="dto">The dto.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
private void AttachPeople(BaseItemDto dto, BaseItem item)
|
||||
/// <param name="user">The requesting user.</param>
|
||||
private void AttachPeople(BaseItemDto dto, BaseItem item, User user = null)
|
||||
{
|
||||
// Ordering by person type to ensure actors and artists are at the front.
|
||||
// This is taking advantage of the fact that they both begin with A
|
||||
@@ -560,6 +561,9 @@ namespace Emby.Server.Implementations.Dto
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null)
|
||||
.Where(i => user == null ?
|
||||
true :
|
||||
i.IsVisible(user))
|
||||
.GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(x => x.First())
|
||||
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.3" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
|
||||
<PackageReference Include="sharpcompress" Version="0.31.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.32.2" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <summary>
|
||||
/// Class WebSocketConnection.
|
||||
/// </summary>
|
||||
public class WebSocketConnection : IWebSocketConnection, IDisposable
|
||||
public class WebSocketConnection : IWebSocketConnection
|
||||
{
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
@@ -36,6 +36,8 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// </summary>
|
||||
private readonly WebSocket _socket;
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
|
||||
/// </summary>
|
||||
@@ -244,10 +246,39 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (dispose)
|
||||
{
|
||||
_socket.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await DisposeAsyncCore().ConfigureAwait(false);
|
||||
Dispose(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
|
||||
/// </summary>
|
||||
/// <returns>A ValueTask.</returns>
|
||||
protected virtual async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
if (_socket.State == WebSocketState.Open)
|
||||
{
|
||||
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "System Shutdown", CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_socket.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1860,7 +1860,9 @@ namespace Emby.Server.Implementations.Library
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
}
|
||||
|
||||
var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
|
||||
var outdated = forceUpdate
|
||||
? item.ImageInfos.Where(i => i.Path != null).ToArray()
|
||||
: item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
|
||||
// Skip image processing if current or live tv source
|
||||
if (outdated.Length == 0 || item.SourceType != SourceType.Library)
|
||||
{
|
||||
@@ -1883,7 +1885,7 @@ namespace Emby.Server.Implementations.Library
|
||||
_logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException || ex is IOException)
|
||||
catch (Exception ex) when (ex is InvalidOperationException or IOException)
|
||||
{
|
||||
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
|
||||
continue;
|
||||
@@ -1895,23 +1897,24 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
ImageDimensions size;
|
||||
try
|
||||
{
|
||||
ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
|
||||
size = _imageProcessor.GetImageDimensions(item, image);
|
||||
image.Width = size.Width;
|
||||
image.Height = size.Height;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
||||
size = new ImageDimensions(0, 0);
|
||||
image.Width = 0;
|
||||
image.Height = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path);
|
||||
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -2450,6 +2453,12 @@ namespace Emby.Server.Implementations.Library
|
||||
return RootFolder;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void QueueLibraryScan()
|
||||
{
|
||||
_taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? GetSeasonNumberFromPath(string path)
|
||||
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
|
||||
@@ -2757,7 +2766,8 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public List<Person> GetPeopleItems(InternalPeopleQuery query)
|
||||
{
|
||||
return _itemRepository.GetPeopleNames(query).Select(i =>
|
||||
return _itemRepository.GetPeopleNames(query)
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -2768,7 +2778,12 @@ namespace Emby.Server.Implementations.Library
|
||||
_logger.LogError(ex, "Error getting person");
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null).ToList();
|
||||
})
|
||||
.Where(i => i != null)
|
||||
.Where(i => query.User == null ?
|
||||
true :
|
||||
i.IsVisible(query.User))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public List<string> GetPeopleNames(InternalPeopleQuery query)
|
||||
|
||||
@@ -13,11 +13,11 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
public static int? GetDefaultAudioStreamIndex(IReadOnlyList<MediaStream> streams, IReadOnlyList<string> preferredLanguages, bool preferDefaultTrack)
|
||||
{
|
||||
var sortedStreams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages);
|
||||
var sortedStreams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages).ToList();
|
||||
|
||||
if (preferDefaultTrack)
|
||||
{
|
||||
var defaultStream = streams.FirstOrDefault(i => i.IsDefault);
|
||||
var defaultStream = sortedStreams.FirstOrDefault(i => i.IsDefault);
|
||||
|
||||
if (defaultStream != null)
|
||||
{
|
||||
|
||||
@@ -225,7 +225,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveVideos<Episode>(parent, files, true, collectionType, true);
|
||||
return ResolveVideos<Episode>(parent, files, false, collectionType, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -183,6 +183,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
private static void SetProviderIdFromPath(Series item, string path)
|
||||
{
|
||||
var justName = Path.GetFileName(path.AsSpan());
|
||||
|
||||
var imdbId = justName.GetAttributeValue("imdbid");
|
||||
if (!string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
item.SetProviderId(MetadataProvider.Imdb, imdbId);
|
||||
}
|
||||
|
||||
var tvdbId = justName.GetAttributeValue("tvdbid");
|
||||
if (!string.IsNullOrEmpty(tvdbId))
|
||||
|
||||
@@ -165,12 +165,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
|
||||
const double DesiredAspect = 2.0 / 3;
|
||||
|
||||
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ??
|
||||
GetProgramImage(ApiUrl, allImages, DesiredAspect);
|
||||
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect, token) ??
|
||||
GetProgramImage(ApiUrl, allImages, DesiredAspect, token);
|
||||
|
||||
const double WideAspect = 16.0 / 9;
|
||||
|
||||
programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect);
|
||||
programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect, token);
|
||||
|
||||
// Don't supply the same image twice
|
||||
if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
|
||||
@@ -178,7 +178,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
programEntry.ThumbImage = null;
|
||||
}
|
||||
|
||||
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect);
|
||||
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect, token);
|
||||
|
||||
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
|
||||
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
|
||||
@@ -399,7 +399,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return info;
|
||||
}
|
||||
|
||||
private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect)
|
||||
private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect, string token)
|
||||
{
|
||||
var match = images
|
||||
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
|
||||
@@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
else
|
||||
{
|
||||
return apiUrl + "/image/" + uri;
|
||||
return apiUrl + "/image/" + uri + "?token=" + token;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,6 +457,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
IReadOnlyList<string> programIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (programIds.Count == 0)
|
||||
{
|
||||
return Array.Empty<ShowImagesDto>();
|
||||
@@ -478,6 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json)
|
||||
};
|
||||
message.Headers.TryAddWithoutValidation("token", token);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none");
|
||||
return VerifyReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), "none");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -386,6 +386,7 @@ namespace Emby.Server.Implementations.Localization
|
||||
yield return new LocalizationOption("Español (Dominicana)", "es_DO");
|
||||
yield return new LocalizationOption("Español (México)", "es-MX");
|
||||
yield return new LocalizationOption("Eesti", "et");
|
||||
yield return new LocalizationOption("Basque", "eu");
|
||||
yield return new LocalizationOption("فارسی", "fa");
|
||||
yield return new LocalizationOption("Suomi", "fi");
|
||||
yield return new LocalizationOption("Filipino", "fil");
|
||||
|
||||
@@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Session
|
||||
{
|
||||
public sealed class WebSocketController : ISessionController, IDisposable
|
||||
public sealed class WebSocketController : ISessionController, IAsyncDisposable, IDisposable
|
||||
{
|
||||
private readonly ILogger<WebSocketController> _logger;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
@@ -99,6 +99,23 @@ namespace Emby.Server.Implementations.Session
|
||||
foreach (var socket in _sockets)
|
||||
{
|
||||
socket.Closed -= OnConnectionClosed;
|
||||
socket.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var socket in _sockets)
|
||||
{
|
||||
socket.Closed -= OnConnectionClosed;
|
||||
await socket.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
@@ -135,20 +135,15 @@ namespace Emby.Server.Implementations.TV
|
||||
return GetResult(episodes, request);
|
||||
}
|
||||
|
||||
public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
|
||||
private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
|
||||
{
|
||||
// Avoid implicitly captured closure
|
||||
var currentUser = user;
|
||||
|
||||
var allNextUp = seriesKeys
|
||||
.Select(i => GetNextUp(i, currentUser, dtoOptions, false));
|
||||
var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false));
|
||||
|
||||
if (request.EnableRewatching)
|
||||
{
|
||||
allNextUp = allNextUp.Concat(
|
||||
seriesKeys.Select(i => GetNextUp(i, currentUser, dtoOptions, true))
|
||||
)
|
||||
.OrderByDescending(i => i.Item1);
|
||||
seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true)))
|
||||
.OrderByDescending(i => i.LastWatchedDate);
|
||||
}
|
||||
|
||||
// If viewing all next up for all series, remove first episodes
|
||||
@@ -161,23 +156,18 @@ namespace Emby.Server.Implementations.TV
|
||||
{
|
||||
if (request.DisableFirstEpisode)
|
||||
{
|
||||
return i.Item1 != DateTime.MinValue;
|
||||
return i.LastWatchedDate != DateTime.MinValue;
|
||||
}
|
||||
|
||||
if (alwaysEnableFirstEpisode || (i.Item1 != DateTime.MinValue && i.Item1.Date >= request.NextUpDateCutoff))
|
||||
if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff))
|
||||
{
|
||||
anyFound = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!anyFound && i.Item1 == DateTime.MinValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return !anyFound && i.LastWatchedDate == DateTime.MinValue;
|
||||
})
|
||||
.Select(i => i.Item2())
|
||||
.Select(i => i.GetEpisodeFunction())
|
||||
.Where(i => i != null);
|
||||
}
|
||||
|
||||
@@ -195,14 +185,13 @@ namespace Emby.Server.Implementations.TV
|
||||
/// Gets the next up.
|
||||
/// </summary>
|
||||
/// <returns>Task{Episode}.</returns>
|
||||
private Tuple<DateTime, Func<Episode>> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
|
||||
private (DateTime LastWatchedDate, Func<Episode> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
|
||||
{
|
||||
var lastQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Descending) },
|
||||
IsPlayed = true,
|
||||
Limit = 1,
|
||||
ParentIndexNumberNotEquals = 0,
|
||||
@@ -213,42 +202,38 @@ namespace Emby.Server.Implementations.TV
|
||||
}
|
||||
};
|
||||
|
||||
if (rewatching)
|
||||
{
|
||||
// find last watched by date played, not by newest episode watched
|
||||
lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) };
|
||||
}
|
||||
// If rewatching is enabled, sort first by date played and then by season and episode numbers
|
||||
lastQuery.OrderBy = rewatching
|
||||
? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
|
||||
: new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
|
||||
|
||||
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
|
||||
|
||||
Func<Episode> getEpisode = () =>
|
||||
Episode GetEpisode()
|
||||
{
|
||||
var nextQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||
OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
|
||||
Limit = 1,
|
||||
IsPlayed = rewatching,
|
||||
IsVirtualItem = false,
|
||||
ParentIndexNumberNotEquals = 0,
|
||||
MinSortName = lastWatchedEpisode?.SortName,
|
||||
DtoOptions = dtoOptions
|
||||
};
|
||||
|
||||
Episode nextEpisode;
|
||||
if (rewatching)
|
||||
// Locate the next up episode based on the last watched episode's season and episode number
|
||||
var lastWatchedParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber;
|
||||
var lastWatchedIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber;
|
||||
if (lastWatchedParentIndexNumber.HasValue && lastWatchedIndexNumber.HasValue)
|
||||
{
|
||||
nextQuery.Limit = 2;
|
||||
// get watched episode after most recently watched
|
||||
nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().ElementAtOrDefault(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
|
||||
nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1);
|
||||
}
|
||||
|
||||
var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
|
||||
|
||||
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
|
||||
{
|
||||
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
@@ -297,7 +282,7 @@ namespace Emby.Server.Implementations.TV
|
||||
}
|
||||
|
||||
return nextEpisode;
|
||||
};
|
||||
}
|
||||
|
||||
if (lastWatchedEpisode != null)
|
||||
{
|
||||
@@ -305,11 +290,11 @@ namespace Emby.Server.Implementations.TV
|
||||
|
||||
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
|
||||
|
||||
return new Tuple<DateTime, Func<Episode>>(lastWatchedDate, getEpisode);
|
||||
return (lastWatchedDate, GetEpisode);
|
||||
}
|
||||
|
||||
// Return the first episode
|
||||
return new Tuple<DateTime, Func<Episode>>(DateTime.MinValue, getEpisode);
|
||||
return (DateTime.MinValue, GetEpisode);
|
||||
}
|
||||
|
||||
private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)
|
||||
|
||||
@@ -324,9 +324,15 @@ namespace Emby.Server.Implementations.Updates
|
||||
}
|
||||
|
||||
_completedInstallationsInternal.Add(package);
|
||||
await _eventManager.PublishAsync(isUpdate
|
||||
? (GenericEventArgs<InstallationInfo>)new PluginUpdatedEventArgs(package)
|
||||
: new PluginInstalledEventArgs(package)).ConfigureAwait(false);
|
||||
|
||||
if (isUpdate)
|
||||
{
|
||||
await _eventManager.PublishAsync(new PluginUpdatedEventArgs(package)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _eventManager.PublishAsync(new PluginInstalledEventArgs(package)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_applicationHost.NotifyPendingRestart();
|
||||
}
|
||||
|
||||
@@ -43,12 +43,16 @@ namespace Jellyfin.Api.Auth
|
||||
/// <param name="ignoreSchedule">Whether to ignore parental control.</param>
|
||||
/// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
|
||||
/// <param name="requiredDownloadPermission">Whether validation requires download permission.</param>
|
||||
/// <param name="requireLiveTvManagementPermission">Whether validation requires LiveTV management permission.</param>
|
||||
/// <param name="requireLiveTvAccessPermission">Whether validation requires LiveTV management permission.</param>
|
||||
/// <returns>Validated claim status.</returns>
|
||||
protected bool ValidateClaims(
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
bool ignoreSchedule = false,
|
||||
bool localAccessOnly = false,
|
||||
bool requiredDownloadPermission = false)
|
||||
bool requiredDownloadPermission = false,
|
||||
bool requireLiveTvManagementPermission = false,
|
||||
bool requireLiveTvAccessPermission = false)
|
||||
{
|
||||
// ApiKey is currently global admin, always allow.
|
||||
var isApiKey = ClaimHelpers.GetIsApiKey(claimsPrincipal);
|
||||
@@ -106,6 +110,20 @@ namespace Jellyfin.Api.Auth
|
||||
return false;
|
||||
}
|
||||
|
||||
// User attempting to access LiveTV without permission.
|
||||
if (requireLiveTvAccessPermission
|
||||
&& !user.HasPermission(PermissionKind.EnableLiveTvAccess))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// User attempting to manage LiveTV without permission.
|
||||
if (requireLiveTvManagementPermission
|
||||
&& !user.HasPermission(PermissionKind.EnableLiveTvManagement))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Jellyfin.Api.Auth.DownloadPolicy
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User);
|
||||
var validated = ValidateClaims(context.User, requiredDownloadPermission: true);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
|
||||
43
Jellyfin.Api/Auth/LiveTvAccessPolicy/LiveTvAccessHandler.cs
Normal file
43
Jellyfin.Api/Auth/LiveTvAccessPolicy/LiveTvAccessHandler.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LiveTvAccessPolicy;
|
||||
|
||||
/// <summary>
|
||||
/// Authorization handler for LiveTV access.
|
||||
/// </summary>
|
||||
public class LiveTvAccessHandler : BaseAuthorizationHandler<LiveTvAccessRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LiveTvAccessHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public LiveTvAccessHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LiveTvAccessRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User, requireLiveTvAccessPermission: true);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LiveTvAccessPolicy;
|
||||
|
||||
/// <summary>
|
||||
/// The LiveTV access requirement.
|
||||
/// </summary>
|
||||
public class LiveTvAccessRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LiveTvManagementPolicy;
|
||||
|
||||
/// <summary>
|
||||
/// Authorization handler for LiveTV management access.
|
||||
/// </summary>
|
||||
public class LiveTvManagementHandler : BaseAuthorizationHandler<LiveTvManagementRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LiveTvManagementHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public LiveTvManagementHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LiveTvManagementRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User, requireLiveTvManagementPermission: true);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LiveTvManagementPolicy;
|
||||
|
||||
/// <summary>
|
||||
/// The LiveTV management requirement.
|
||||
/// </summary>
|
||||
public class LiveTvManagementRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
@@ -74,5 +74,15 @@ namespace Jellyfin.Api.Constants
|
||||
/// Policy name for accessing a SyncPlay group.
|
||||
/// </summary>
|
||||
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for accessing LiveTV.
|
||||
/// </summary>
|
||||
public const string LiveTvAccess = "LiveTvAccess";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for managing LiveTV.
|
||||
/// </summary>
|
||||
public const string LiveTvManagement = "LiveTvManagement";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,18 +91,18 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetAudioStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -132,8 +132,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -262,12 +262,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -297,8 +297,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
|
||||
@@ -125,12 +125,14 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
|
||||
/// <response code="204">Media encoder path updated.</response>
|
||||
/// <returns>Status.</returns>
|
||||
[Obsolete("This endpoint is obsolete.")]
|
||||
[HttpPost("MediaEncoder/Path")]
|
||||
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
|
||||
{
|
||||
_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
|
||||
// API ENDPOINT DISABLED (NOOP) FOR SECURITY PURPOSES
|
||||
//_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.PlaybackDtos;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.MediaEncoding.Hls.Playlist;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -21,6 +22,7 @@ using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.MediaEncoding.Encoder;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -172,18 +174,18 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesPlaylistFile]
|
||||
public async Task<ActionResult> GetLiveHlsStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -213,8 +215,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -285,7 +287,7 @@ namespace Jellyfin.Api.Controllers
|
||||
// Due to CTS.Token calling ThrowIfDisposed (https://github.com/dotnet/runtime/issues/29970) we have to "cache" the token
|
||||
// since it gets disposed when ffmpeg exits
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
using var state = await StreamingHelpers.GetStreamingState(
|
||||
var state = await StreamingHelpers.GetStreamingState(
|
||||
streamingRequest,
|
||||
Request,
|
||||
_authContext,
|
||||
@@ -424,12 +426,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery, Required] string mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -461,8 +463,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -594,12 +596,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery, Required] string mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -630,8 +632,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -760,12 +762,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -797,8 +799,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -928,12 +930,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -964,8 +966,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -1105,12 +1107,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -1142,8 +1144,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -1286,12 +1288,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -1322,8 +1324,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -1414,7 +1416,8 @@ namespace Jellyfin.Api.Controllers
|
||||
state.RunTimeTicks ?? 0,
|
||||
state.Request.SegmentContainer ?? string.Empty,
|
||||
"hls1/main/",
|
||||
Request.QueryString.ToString());
|
||||
Request.QueryString.ToString(),
|
||||
EncodingHelper.IsCopyCodec(state.OutputVideoCodec));
|
||||
var playlist = _dynamicHlsPlaylistGenerator.CreateMainPlaylist(request);
|
||||
|
||||
return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
|
||||
@@ -1431,7 +1434,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
using var state = await StreamingHelpers.GetStreamingState(
|
||||
var state = await StreamingHelpers.GetStreamingState(
|
||||
streamingRequest,
|
||||
Request,
|
||||
_authContext,
|
||||
@@ -1661,8 +1664,8 @@ namespace Jellyfin.Api.Controllers
|
||||
startNumber.ToString(CultureInfo.InvariantCulture),
|
||||
baseUrlParam,
|
||||
isEventPlaylist ? "event" : "vod",
|
||||
outputTsArg,
|
||||
outputPath).Trim();
|
||||
EncodingUtils.NormalizePath(outputTsArg),
|
||||
EncodingUtils.NormalizePath(outputPath)).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1692,7 +1695,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
audioTranscodeParams += "-acodec " + audioCodec;
|
||||
|
||||
if (state.OutputAudioBitrate.HasValue)
|
||||
if (state.OutputAudioBitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(state.ActualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -1711,11 +1714,13 @@ namespace Jellyfin.Api.Controllers
|
||||
return audioTranscodeParams;
|
||||
}
|
||||
|
||||
// flac and opus are experimental in mp4 muxer
|
||||
// dts, flac, opus and truehd are experimental in mp4 muxer
|
||||
var strictArgs = string.Empty;
|
||||
|
||||
if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||
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";
|
||||
}
|
||||
@@ -1744,8 +1749,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
var bitrate = state.OutputAudioBitrate;
|
||||
|
||||
if (bitrate.HasValue)
|
||||
if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -1789,7 +1793,8 @@ namespace Jellyfin.Api.Controllers
|
||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (EncodingHelper.IsCopyCodec(codec)
|
||||
&& (string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
||||
&& (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)))
|
||||
{
|
||||
@@ -1839,7 +1844,11 @@ namespace Jellyfin.Api.Controllers
|
||||
// args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
|
||||
|
||||
// video processing filters.
|
||||
args += _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
|
||||
var videoProcessParam = _encodingHelper.GetVideoProcessingFilterParam(state, _encodingOptions, codec);
|
||||
|
||||
var negativeMapArgs = _encodingHelper.GetNegativeMapArgsByFilters(state, videoProcessParam);
|
||||
|
||||
args = negativeMapArgs + args + videoProcessParam;
|
||||
|
||||
// -start_at_zero is necessary to use with -ss when seeking,
|
||||
// otherwise the target position cannot be determined.
|
||||
|
||||
@@ -1724,6 +1724,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery, Range(0, 100)] int quality = 90)
|
||||
{
|
||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||
if (!brandingOptions.SplashscreenEnabled)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
string splashscreenPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(brandingOptions.SplashscreenLocation)
|
||||
@@ -1776,6 +1781,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a custom splashscreen.
|
||||
/// The body is expected to the image contents base64 encoded.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
/// <response code="204">Successfully uploaded new splashscreen.</response>
|
||||
@@ -1799,7 +1805,13 @@ namespace Jellyfin.Api.Controllers
|
||||
return BadRequest("Error reading mimetype from uploaded image");
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + MimeTypes.ToExtension(mimeType.Value));
|
||||
var extension = MimeTypes.ToExtension(mimeType.Value);
|
||||
if (string.IsNullOrEmpty(extension))
|
||||
{
|
||||
return BadRequest("Error converting mimetype to an image extension");
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
|
||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||
brandingOptions.SplashscreenLocation = filePath;
|
||||
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
|
||||
@@ -1812,6 +1824,29 @@ namespace Jellyfin.Api.Controllers
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a custom splashscreen.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
/// <response code="204">Successfully deleted the custom splashscreen.</response>
|
||||
/// <response code="403">User does not have permission to delete splashscreen..</response>
|
||||
[HttpDelete("Branding/Splashscreen")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DeleteCustomSplashscreen()
|
||||
{
|
||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||
if (!string.IsNullOrEmpty(brandingOptions.SplashscreenLocation)
|
||||
&& System.IO.File.Exists(brandingOptions.SplashscreenLocation))
|
||||
{
|
||||
System.IO.File.Delete(brandingOptions.SplashscreenLocation);
|
||||
brandingOptions.SplashscreenLocation = null;
|
||||
_serverConfigurationManager.SaveConfiguration("branding", brandingOptions);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
|
||||
{
|
||||
using var reader = new StreamReader(inputStream);
|
||||
|
||||
@@ -270,30 +270,13 @@ namespace Jellyfin.Api.Controllers
|
||||
includeItemTypes = new[] { BaseItemKind.Playlist };
|
||||
}
|
||||
|
||||
var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
|
||||
|
||||
bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1
|
||||
// Assume all folders inside an EnabledChannel are enabled
|
||||
|| Array.IndexOf(enabledChannels, item.Id) != -1
|
||||
// Assume all items inside an EnabledChannel are enabled
|
||||
|| Array.IndexOf(enabledChannels, item.ChannelId) != -1;
|
||||
|
||||
var collectionFolders = _libraryManager.GetCollectionFolders(item);
|
||||
foreach (var collectionFolder in collectionFolders)
|
||||
{
|
||||
if (user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
|
||||
{
|
||||
isInEnabledFolder = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (item is not UserRootFolder
|
||||
&& !isInEnabledFolder
|
||||
&& !user.HasPermission(PermissionKind.EnableAllFolders)
|
||||
&& !user.HasPermission(PermissionKind.EnableAllChannels)
|
||||
&& !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
|
||||
// api keys can always access all folders
|
||||
&& !ClaimHelpers.GetIsApiKey(User)
|
||||
// check the item is visible for the user
|
||||
&& !item.IsVisible(user))
|
||||
{
|
||||
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
|
||||
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user!.Username, item.Name);
|
||||
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
|
||||
}
|
||||
|
||||
|
||||
@@ -492,7 +492,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Media folders returned.</response>
|
||||
/// <returns>List of user media folders.</returns>
|
||||
[HttpGet("Library/MediaFolders")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
|
||||
{
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Info")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<LiveTvInfo> GetLiveTvInfo()
|
||||
{
|
||||
return _liveTvManager.GetLiveTvInfo(CancellationToken.None);
|
||||
@@ -129,7 +129,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Channels")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetLiveTvChannels(
|
||||
[FromQuery] ChannelType? type,
|
||||
[FromQuery] Guid? userId,
|
||||
@@ -208,7 +208,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the live tv channel.</returns>
|
||||
[HttpGet("Channels/{channelId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
|
||||
{
|
||||
var user = userId is null || userId.Value.Equals(default)
|
||||
@@ -249,7 +249,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
|
||||
[HttpGet("Recordings")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetRecordings(
|
||||
[FromQuery] string? channelId,
|
||||
[FromQuery] Guid? userId,
|
||||
@@ -320,7 +320,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the live tv recordings.</returns>
|
||||
[HttpGet("Recordings/Series")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[Obsolete("This endpoint is obsolete.")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "channelId", Justification = "Imported from ServiceStack")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
|
||||
@@ -363,7 +363,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the recording groups.</returns>
|
||||
[HttpGet("Recordings/Groups")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[Obsolete("This endpoint is obsolete.")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetRecordingGroups([FromQuery] Guid? userId)
|
||||
@@ -379,7 +379,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the recording folders.</returns>
|
||||
[HttpGet("Recordings/Folders")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetRecordingFolders([FromQuery] Guid? userId)
|
||||
{
|
||||
var user = userId is null || userId.Value.Equals(default)
|
||||
@@ -401,7 +401,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the live tv recording.</returns>
|
||||
[HttpGet("Recordings/{recordingId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
|
||||
{
|
||||
var user = userId is null || userId.Value.Equals(default)
|
||||
@@ -423,10 +423,9 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Tuners/{tunerId}/Reset")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -441,7 +440,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Timers/{timerId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public async Task<ActionResult<TimerInfoDto>> GetTimer([FromRoute, Required] string timerId)
|
||||
{
|
||||
return await _liveTvManager.GetTimer(timerId, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -457,7 +456,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Timers/Defaults")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public async Task<ActionResult<SeriesTimerInfoDto>> GetDefaultTimer([FromQuery] string? programId)
|
||||
{
|
||||
return string.IsNullOrEmpty(programId)
|
||||
@@ -477,7 +476,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Timers")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public async Task<ActionResult<QueryResult<TimerInfoDto>>> GetTimers(
|
||||
[FromQuery] string? channelId,
|
||||
[FromQuery] string? seriesTimerId,
|
||||
@@ -531,7 +530,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Programs")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
|
||||
[FromQuery] Guid? userId,
|
||||
@@ -614,7 +613,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpPost("Programs")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
|
||||
{
|
||||
var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
|
||||
@@ -680,7 +679,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Recommended epgs returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the queryresult of recommended epgs.</returns>
|
||||
[HttpGet("Programs/Recommended")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecommendedPrograms(
|
||||
[FromQuery] Guid? userId,
|
||||
@@ -732,7 +731,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Program returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the livetv program.</returns>
|
||||
[HttpGet("Programs/{programId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<BaseItemDto>> GetProgram(
|
||||
[FromRoute, Required] string programId,
|
||||
@@ -753,13 +752,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpDelete("Recordings/{recordingId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteRecording([FromRoute, Required] Guid recordingId)
|
||||
public ActionResult DeleteRecording([FromRoute, Required] Guid recordingId)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
|
||||
var item = _libraryManager.GetItemById(recordingId);
|
||||
if (item == null)
|
||||
{
|
||||
@@ -781,11 +778,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Timer deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("Timers/{timerId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CancelTimer([FromRoute, Required] string timerId)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CancelTimer(timerId).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -798,12 +794,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Timer updated.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Timers/{timerId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> UpdateTimer([FromRoute, Required] string timerId, [FromBody] TimerInfoDto timerInfo)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.UpdateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -815,11 +810,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Timer created.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Timers")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CreateTimer([FromBody] TimerInfoDto timerInfo)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CreateTimer(timerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -832,7 +826,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="404">Series timer not found.</response>
|
||||
/// <returns>A <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if timer not found.</returns>
|
||||
[HttpGet("SeriesTimers/{timerId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<SeriesTimerInfoDto>> GetSeriesTimer([FromRoute, Required] string timerId)
|
||||
@@ -854,7 +848,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Timers returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> of live tv series timers.</returns>
|
||||
[HttpGet("SeriesTimers")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<QueryResult<SeriesTimerInfoDto>>> GetSeriesTimers([FromQuery] string? sortBy, [FromQuery] SortOrder? sortOrder)
|
||||
{
|
||||
@@ -874,11 +868,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Timer cancelled.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("SeriesTimers/{timerId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CancelSeriesTimer([FromRoute, Required] string timerId)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CancelSeriesTimer(timerId).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -891,12 +884,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Series timer updated.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("SeriesTimers/{timerId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "timerId", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> UpdateSeriesTimer([FromRoute, Required] string timerId, [FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.UpdateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -908,11 +900,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Series timer info created.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("SeriesTimers")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> CreateSeriesTimer([FromBody] SeriesTimerInfoDto seriesTimerInfo)
|
||||
{
|
||||
await AssertUserCanManageLiveTv().ConfigureAwait(false);
|
||||
await _liveTvManager.CreateSeriesTimer(seriesTimerInfo, CancellationToken.None).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
@@ -923,7 +914,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="groupId">Group id.</param>
|
||||
/// <returns>A <see cref="NotFoundResult"/>.</returns>
|
||||
[HttpGet("Recordings/Groups/{groupId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("This endpoint is obsolete.")]
|
||||
public ActionResult<BaseItemDto> GetRecordingGroup([FromRoute, Required] Guid groupId)
|
||||
@@ -937,7 +928,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Guid info returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the guide info.</returns>
|
||||
[HttpGet("GuideInfo")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<GuideInfo> GetGuideInfo()
|
||||
{
|
||||
@@ -951,7 +942,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Created tuner host returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns>
|
||||
[HttpPost("TunerHosts")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
|
||||
{
|
||||
@@ -965,7 +956,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Tuner host deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("TunerHosts")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DeleteTunerHost([FromQuery] string? id)
|
||||
{
|
||||
@@ -981,7 +972,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Default listings provider info returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the default listings provider info.</returns>
|
||||
[HttpGet("ListingProviders/Default")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<ListingsProviderInfo> GetDefaultListingProvider()
|
||||
{
|
||||
@@ -998,7 +989,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Created listings provider returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns>
|
||||
[HttpPost("ListingProviders")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult<ListingsProviderInfo>> AddListingProvider(
|
||||
@@ -1025,7 +1016,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Listing provider deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("ListingProviders")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DeleteListingProvider([FromQuery] string? id)
|
||||
{
|
||||
@@ -1043,7 +1034,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Available lineups returned.</response>
|
||||
/// <returns>A <see cref="OkResult"/> containing the available lineups.</returns>
|
||||
[HttpGet("ListingProviders/Lineups")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IEnumerable<NameIdPair>>> GetLineups(
|
||||
[FromQuery] string? id,
|
||||
@@ -1060,7 +1051,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Available countries returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the available countries.</returns>
|
||||
[HttpGet("ListingProviders/SchedulesDirect/Countries")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile(MediaTypeNames.Application.Json)]
|
||||
public async Task<ActionResult> GetSchedulesDirectCountries()
|
||||
@@ -1081,7 +1072,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Channel mapping options returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns>
|
||||
[HttpGet("ChannelMappingOptions")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
|
||||
{
|
||||
@@ -1119,7 +1110,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Created channel mapping returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
|
||||
[HttpPost("ChannelMappings")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
|
||||
{
|
||||
@@ -1132,7 +1123,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Tuner host types returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the tuner host types.</returns>
|
||||
[HttpGet("TunerHosts/Types")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
|
||||
{
|
||||
@@ -1147,7 +1138,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
|
||||
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
|
||||
[HttpGet("Tuners/Discover")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
|
||||
{
|
||||
@@ -1207,20 +1198,5 @@ namespace Jellyfin.Api.Controllers
|
||||
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetStream());
|
||||
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
|
||||
}
|
||||
|
||||
private async Task AssertUserCanManageLiveTv()
|
||||
{
|
||||
var user = await _sessionContext.GetUser(Request).ConfigureAwait(false);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new SecurityException("Anonymous live tv management is not allowed.");
|
||||
}
|
||||
|
||||
if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
|
||||
{
|
||||
throw new SecurityException("The current user does not have permission to manage live tv.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,10 @@ namespace Jellyfin.Api.Controllers
|
||||
Limit = limit ?? 0
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>(peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray());
|
||||
return new QueryResult<BaseItemDto>(
|
||||
peopleItems
|
||||
.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -187,7 +188,7 @@ namespace Jellyfin.Api.Controllers
|
||||
result.AlbumArtist = album.AlbumArtist;
|
||||
break;
|
||||
case Audio song:
|
||||
result.AlbumArtist = song.AlbumArtists?[0];
|
||||
result.AlbumArtist = song.AlbumArtists?.FirstOrDefault();
|
||||
result.Artists = song.Artists;
|
||||
|
||||
MusicAlbum musicAlbum = song.AlbumEntity;
|
||||
|
||||
@@ -102,13 +102,13 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] int? transcodingAudioChannels,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] string? transcodingContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? transcodingContainer,
|
||||
[FromQuery] string? transcodingProtocol,
|
||||
[FromQuery] int? maxAudioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
@@ -117,7 +117,13 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool enableRedirection = true)
|
||||
{
|
||||
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
||||
(await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId;
|
||||
var authorizationInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
authorizationInfo.DeviceId = deviceId;
|
||||
|
||||
if (!userId.HasValue || userId.Value.Equals(Guid.Empty))
|
||||
{
|
||||
userId = authorizationInfo.UserId;
|
||||
}
|
||||
|
||||
var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -282,16 +282,19 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var success = await _userManager.AuthenticateUser(
|
||||
user.Username,
|
||||
request.CurrentPw,
|
||||
request.CurrentPw,
|
||||
HttpContext.GetNormalizedRemoteIp().ToString(),
|
||||
false).ConfigureAwait(false);
|
||||
|
||||
if (success == null)
|
||||
if (!HttpContext.User.IsInRole(UserRoles.Administrator))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
|
||||
var success = await _userManager.AuthenticateUser(
|
||||
user.Username,
|
||||
request.CurrentPw,
|
||||
request.CurrentPw,
|
||||
HttpContext.GetNormalizedRemoteIp().ToString(),
|
||||
false).ConfigureAwait(false);
|
||||
|
||||
if (success == null)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
|
||||
}
|
||||
}
|
||||
|
||||
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
|
||||
|
||||
@@ -318,18 +318,18 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetVideoStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -361,8 +361,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
@@ -427,7 +427,7 @@ namespace Jellyfin.Api.Controllers
|
||||
StreamOptions = streamOptions
|
||||
};
|
||||
|
||||
using var state = await StreamingHelpers.GetStreamingState(
|
||||
var state = await StreamingHelpers.GetStreamingState(
|
||||
streamingRequest,
|
||||
Request,
|
||||
_authContext,
|
||||
@@ -578,12 +578,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
@@ -615,8 +615,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
|
||||
[FromQuery][RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
|
||||
[FromQuery] string? transcodeReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
|
||||
@@ -8,6 +8,8 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -202,8 +204,18 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
if (state.VideoStream != null && state.VideoRequest != null)
|
||||
{
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
|
||||
// Provide SDR HEVC entrance for backward compatibility.
|
||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
if (encodingOptions.AllowHevcEncoding
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& !string.IsNullOrEmpty(state.VideoStream.VideoRange)
|
||||
&& string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -216,11 +228,26 @@ namespace Jellyfin.Api.Helpers
|
||||
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
|
||||
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
||||
|
||||
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0;
|
||||
var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
|
||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
|
||||
var sdrOutputAudioBitrate = 0;
|
||||
if (EncodingHelper.LosslessAudioCodecs.Contains(state.VideoRequest.AudioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sdrOutputAudioBitrate = state.AudioStream.BitRate ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream, state.OutputAudioChannels) ?? 0;
|
||||
}
|
||||
|
||||
AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||
var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
|
||||
// Restore the video codec
|
||||
state.OutputVideoCodec = "copy";
|
||||
@@ -250,6 +277,13 @@ namespace Jellyfin.Api.Helpers
|
||||
state.VideoStream.Level = originalLevel;
|
||||
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
||||
builder.Append(newPlaylist);
|
||||
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,6 +642,11 @@ namespace Jellyfin.Api.Helpers
|
||||
return HlsCodecStringHelpers.GetALACString();
|
||||
}
|
||||
|
||||
if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HlsCodecStringHelpers.GetOPUSString();
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
@@ -706,7 +745,19 @@ namespace Jellyfin.Api.Helpers
|
||||
return oldPlaylist.Replace(
|
||||
oldValue.ToString(),
|
||||
newValue.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
|
||||
{
|
||||
if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
|
||||
|
||||
return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,18 @@ namespace Jellyfin.Api.Helpers
|
||||
/// <summary>
|
||||
/// Codec name for FLAC.
|
||||
/// </summary>
|
||||
public const string FLAC = "fLaC";
|
||||
public const string FLAC = "flac";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for ALAC.
|
||||
/// </summary>
|
||||
public const string ALAC = "alac";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for OPUS.
|
||||
/// </summary>
|
||||
public const string OPUS = "opus";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a MP3 codec string.
|
||||
/// </summary>
|
||||
@@ -101,6 +106,15 @@ namespace Jellyfin.Api.Helpers
|
||||
return ALAC;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an OPUS codec string.
|
||||
/// </summary>
|
||||
/// <returns>OPUS codec string.</returns>
|
||||
public static string GetOPUSString()
|
||||
{
|
||||
return OPUS;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a H.264 codec string.
|
||||
/// </summary>
|
||||
|
||||
@@ -179,15 +179,21 @@ namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
containerInternal = streamingRequest.Static ?
|
||||
StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
|
||||
: GetOutputFileExtension(state);
|
||||
: GetOutputFileExtension(state, mediaSource);
|
||||
}
|
||||
|
||||
var outputAudioCodec = streamingRequest.AudioCodec;
|
||||
if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec))
|
||||
{
|
||||
state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
|
||||
}
|
||||
|
||||
state.OutputAudioCodec = outputAudioCodec;
|
||||
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
||||
|
||||
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
|
||||
|
||||
state.OutputAudioCodec = streamingRequest.AudioCodec;
|
||||
|
||||
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
|
||||
|
||||
if (state.VideoRequest != null)
|
||||
@@ -235,7 +241,7 @@ namespace Jellyfin.Api.Helpers
|
||||
ApplyDeviceProfileSettings(state, dlnaManager, deviceManager, httpRequest, streamingRequest.DeviceProfileId, streamingRequest.Static);
|
||||
|
||||
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
||||
? GetOutputFileExtension(state)
|
||||
? GetOutputFileExtension(state, mediaSource)
|
||||
: ("." + state.OutputContainer);
|
||||
|
||||
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||
@@ -312,7 +318,7 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
responseHeaders.Add(
|
||||
"contentFeatures.dlna.org",
|
||||
ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
|
||||
ContentFeatureBuilder.BuildVideoHeader(profile, state.OutputContainer, videoCodec, audioCodec, state.OutputWidth, state.OutputHeight, state.TargetVideoBitDepth, state.OutputVideoBitrate, state.TargetTimestamp, isStaticallyStreamed, state.RunTimeTicks, state.TargetVideoProfile, state.TargetVideoRangeType, state.TargetVideoLevel, state.TargetFramerate, state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, state.IsTargetInterlaced, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, state.IsTargetAVC).FirstOrDefault() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,8 +415,9 @@ namespace Jellyfin.Api.Helpers
|
||||
/// Gets the output file extension.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="mediaSource">The mediaSource.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string? GetOutputFileExtension(StreamState state)
|
||||
private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
|
||||
{
|
||||
var ext = Path.GetExtension(state.RequestedUrl);
|
||||
|
||||
@@ -425,7 +432,7 @@ namespace Jellyfin.Api.Helpers
|
||||
var videoCodec = state.Request.VideoCodec;
|
||||
|
||||
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
||||
string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ".ts";
|
||||
}
|
||||
@@ -474,6 +481,13 @@ namespace Jellyfin.Api.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the container of mediaSource
|
||||
if (!string.IsNullOrEmpty(mediaSource?.Container))
|
||||
{
|
||||
var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase);
|
||||
return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -533,6 +547,7 @@ namespace Jellyfin.Api.Helpers
|
||||
state.TargetVideoBitDepth,
|
||||
state.OutputVideoBitrate,
|
||||
state.TargetVideoProfile,
|
||||
state.TargetVideoRangeType,
|
||||
state.TargetVideoLevel,
|
||||
state.TargetFramerate,
|
||||
state.TargetPacketLength,
|
||||
|
||||
@@ -654,8 +654,8 @@ namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
if (EnableThrottling(state))
|
||||
{
|
||||
transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem);
|
||||
state.TranscodingThrottler.Start();
|
||||
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
||||
transcodingJob.TranscodingThrottler.Start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,18 +663,11 @@ namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
|
||||
// enable throttling when NOT using hardware acceleration
|
||||
if (string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
|
||||
{
|
||||
return state.InputProtocol == MediaProtocol.File &&
|
||||
state.RunTimeTicks.HasValue &&
|
||||
state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
|
||||
state.IsInputVideo &&
|
||||
state.VideoType == VideoType.VideoFile &&
|
||||
!EncodingHelper.IsCopyCodec(state.OutputVideoCodec);
|
||||
}
|
||||
|
||||
return false;
|
||||
return state.InputProtocol == MediaProtocol.File &&
|
||||
state.RunTimeTicks.HasValue &&
|
||||
state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
|
||||
state.IsInputVideo &&
|
||||
state.VideoType == VideoType.VideoFile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" />
|
||||
@@ -26,6 +26,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
|
||||
<ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -17,6 +18,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
||||
private readonly ILogger<TranscodingThrottler> _logger;
|
||||
private readonly IConfigurationManager _config;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private Timer? _timer;
|
||||
private bool _isPaused;
|
||||
|
||||
@@ -27,12 +29,14 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param>
|
||||
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem)
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_job = job;
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_fileSystem = fileSystem;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,7 +59,8 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
||||
|
||||
try
|
||||
{
|
||||
await _job.Process!.StandardInput.WriteLineAsync().ConfigureAwait(false);
|
||||
var resumeKey = _mediaEncoder.IsPkeyPauseSupported ? "u" : Environment.NewLine;
|
||||
await _job.Process!.StandardInput.WriteAsync(resumeKey).ConfigureAwait(false);
|
||||
_isPaused = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -125,11 +130,13 @@ namespace Jellyfin.Api.Models.PlaybackDtos
|
||||
{
|
||||
if (!_isPaused)
|
||||
{
|
||||
_logger.LogDebug("Sending pause command to ffmpeg");
|
||||
var pauseKey = _mediaEncoder.IsPkeyPauseSupported ? "p" : "c";
|
||||
|
||||
_logger.LogDebug("Sending pause command [{Key}] to ffmpeg", pauseKey);
|
||||
|
||||
try
|
||||
{
|
||||
await _job.Process!.StandardInput.WriteAsync("c").ConfigureAwait(false);
|
||||
await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false);
|
||||
_isPaused = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -47,11 +47,6 @@ namespace Jellyfin.Api.Models.StreamingDtos
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transcoding throttler.
|
||||
/// </summary>
|
||||
public TranscodingThrottler? TranscodingThrottler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the video request.
|
||||
/// </summary>
|
||||
@@ -191,11 +186,8 @@ namespace Jellyfin.Api.Models.StreamingDtos
|
||||
{
|
||||
_mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
TranscodingThrottler?.Dispose();
|
||||
}
|
||||
|
||||
TranscodingThrottler = null;
|
||||
TranscodingJob = null;
|
||||
|
||||
_disposed = true;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.8.0</VersionPrefix>
|
||||
<VersionPrefix>10.8.13</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BlurHashSharp" Version="1.2.0" />
|
||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.1" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.1" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.2" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.2" />
|
||||
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,7 +10,7 @@ using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SkiaSharp;
|
||||
using static Jellyfin.Drawing.Skia.SkiaHelper;
|
||||
using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia
|
||||
{
|
||||
@@ -19,8 +19,7 @@ namespace Jellyfin.Drawing.Skia
|
||||
/// </summary>
|
||||
public class SkiaEncoder : IImageEncoder
|
||||
{
|
||||
private static readonly HashSet<string> _transparentImageTypes
|
||||
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
||||
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
||||
|
||||
private readonly ILogger<SkiaEncoder> _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
@@ -71,7 +70,7 @@ namespace Jellyfin.Drawing.Skia
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
|
||||
=> new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
|
||||
=> new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
|
||||
|
||||
/// <summary>
|
||||
/// Check if the native lib is available.
|
||||
@@ -109,9 +108,7 @@ namespace Jellyfin.Drawing.Skia
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <exception cref="ArgumentNullException">The path is null.</exception>
|
||||
/// <exception cref="FileNotFoundException">The path is not valid.</exception>
|
||||
/// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
|
||||
public ImageDimensions GetImageSize(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
@@ -119,12 +116,27 @@ namespace Jellyfin.Drawing.Skia
|
||||
throw new FileNotFoundException("File not found", path);
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var svg = new SKSvg();
|
||||
svg.Load(path);
|
||||
return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height));
|
||||
}
|
||||
|
||||
using var codec = SKCodec.Create(path, out SKCodecResult result);
|
||||
EnsureSuccess(result);
|
||||
|
||||
var info = codec.Info;
|
||||
|
||||
return new ImageDimensions(info.Width, info.Height);
|
||||
switch (result)
|
||||
{
|
||||
case SKCodecResult.Success:
|
||||
var info = codec.Info;
|
||||
return new ImageDimensions(info.Width, info.Height);
|
||||
case SKCodecResult.Unimplemented:
|
||||
_logger.LogDebug("Image format not supported: {FilePath}", path);
|
||||
return new ImageDimensions(0, 0);
|
||||
default:
|
||||
_logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
|
||||
return new ImageDimensions(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -138,6 +150,13 @@ namespace Jellyfin.Drawing.Skia
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
|
||||
if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Any larger than 128x128 is too slow and there's no visually discernible difference
|
||||
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
|
||||
}
|
||||
@@ -378,6 +397,13 @@ namespace Jellyfin.Drawing.Skia
|
||||
throw new ArgumentException("String can't be empty.", nameof(outputPath));
|
||||
}
|
||||
|
||||
var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
|
||||
if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
|
||||
return inputPath;
|
||||
}
|
||||
|
||||
var skiaOutputFormat = GetImageFormat(outputFormat);
|
||||
|
||||
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
|
||||
|
||||
@@ -8,19 +8,6 @@ namespace Jellyfin.Drawing.Skia
|
||||
/// </summary>
|
||||
public static class SkiaHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures the result is a success
|
||||
/// by throwing an exception when that's not the case.
|
||||
/// </summary>
|
||||
/// <param name="result">The result returned by Skia.</param>
|
||||
public static void EnsureSuccess(SKCodecResult result)
|
||||
{
|
||||
if (result != SKCodecResult.Success)
|
||||
{
|
||||
throw new SkiaCodecException(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next valid image as a bitmap.
|
||||
/// </summary>
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.5">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
657
Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
generated
Normal file
657
Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
generated
Normal file
@@ -0,0 +1,657 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
[DbContext(typeof(JellyfinDb))]
|
||||
[Migration("20221022080052_AddIndexActivityLogsDateCreated")]
|
||||
partial class AddIndexActivityLogsDateCreated
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("jellyfin")
|
||||
.HasAnnotation("ProductVersion", "6.0.9");
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DayOfWeek")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<double>("EndHour")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<double>("StartHour")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AccessSchedules", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ItemId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LogSeverity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Overview")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ShortOverview")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DateCreated");
|
||||
|
||||
b.ToTable("ActivityLogs", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CustomItemDisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChromecastVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DashboardTheme")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EnableNextVideoInfoOverlay")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("IndexBy")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ScrollDirection")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ShowBackdrop")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ShowSidebar")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SkipBackwardLength")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SkipForwardLength")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TvHome")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "ItemId", "Client")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("DisplayPreferencesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DisplayPreferencesId");
|
||||
|
||||
b.ToTable("HomeSection", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ImageInfos", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("IndexBy")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("ItemId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("RememberIndexing")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberSorting")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SortBy")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ViewType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ItemDisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("Permission_Permissions_Guid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Value")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "Kind")
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Permissions", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("Preference_Preferences_Guid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "Kind")
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Preferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateLastActivity")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccessToken")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ApiKeys", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AppName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AppVersion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateLastActivity")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.HasIndex("AccessToken", "DateLastActivity");
|
||||
|
||||
b.HasIndex("DeviceId", "DateLastActivity");
|
||||
|
||||
b.HasIndex("UserId", "DeviceId");
|
||||
|
||||
b.ToTable("Devices", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CustomName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DeviceOptions", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AudioLanguagePreference")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AuthenticationProviderId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DisplayCollectionsView")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("DisplayMissingEpisodes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("EasyPassword")
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EnableAutoLogin")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableLocalPassword")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableNextEpisodeAutoPlay")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("EnableUserPreferenceAccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("HidePlayedInLatest")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("InternalId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("InvalidLoginAttemptCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastActivityDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastLoginDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("LoginAttemptsBeforeLockout")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxActiveSessions")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("MaxParentalAgeRating")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("MustUpdatePassword")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasMaxLength(65535)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordResetProviderId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PlayDefaultAudioTrack")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberAudioSelections")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RememberSubtitleSelections")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("RemoteClientBitrateLimit")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SubtitleLanguagePreference")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SubtitleMode")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SyncPlayAccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.UseCollation("NOCASE");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("AccessSchedules")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("DisplayPreferences")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
|
||||
.WithMany("HomeSections")
|
||||
.HasForeignKey("DisplayPreferencesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithOne("ProfileImage")
|
||||
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("ItemDisplayPreferences")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("Permissions")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", null)
|
||||
.WithMany("Preferences")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
||||
{
|
||||
b.HasOne("Jellyfin.Data.Entities.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
{
|
||||
b.Navigation("HomeSections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("AccessSchedules");
|
||||
|
||||
b.Navigation("DisplayPreferences");
|
||||
|
||||
b.Navigation("ItemDisplayPreferences");
|
||||
|
||||
b.Navigation("Permissions");
|
||||
|
||||
b.Navigation("Preferences");
|
||||
|
||||
b.Navigation("ProfileImage");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma warning disable CS1591, SA1601
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
public partial class AddIndexActivityLogsDateCreated : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ActivityLogs_DateCreated",
|
||||
schema: "jellyfin",
|
||||
table: "ActivityLogs",
|
||||
column: "DateCreated");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ActivityLogs_DateCreated",
|
||||
schema: "jellyfin",
|
||||
table: "ActivityLogs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
[DbContext(typeof(JellyfinDb))]
|
||||
@@ -15,7 +17,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("jellyfin")
|
||||
.HasAnnotation("ProductVersion", "5.0.7");
|
||||
.HasAnnotation("ProductVersion", "6.0.9");
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
{
|
||||
@@ -39,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AccessSchedules");
|
||||
b.ToTable("AccessSchedules", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
|
||||
@@ -85,7 +87,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ActivityLogs");
|
||||
b.HasIndex("DateCreated");
|
||||
|
||||
b.ToTable("ActivityLogs", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
|
||||
@@ -117,7 +121,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("UserId", "ItemId", "Client", "Key")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CustomItemDisplayPreferences");
|
||||
b.ToTable("CustomItemDisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
|
||||
@@ -174,7 +178,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("UserId", "ItemId", "Client")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DisplayPreferences");
|
||||
b.ToTable("DisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
|
||||
@@ -196,7 +200,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasIndex("DisplayPreferencesId");
|
||||
|
||||
b.ToTable("HomeSection");
|
||||
b.ToTable("HomeSection", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
|
||||
@@ -221,7 +225,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ImageInfos");
|
||||
b.ToTable("ImageInfos", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
|
||||
@@ -265,7 +269,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ItemDisplayPreferences");
|
||||
b.ToTable("ItemDisplayPreferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
|
||||
@@ -296,7 +300,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Permissions");
|
||||
b.ToTable("Permissions", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
|
||||
@@ -329,7 +333,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Preferences");
|
||||
b.ToTable("Preferences", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
|
||||
@@ -358,7 +362,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("AccessToken")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
b.ToTable("ApiKeys", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
|
||||
@@ -416,7 +420,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.HasIndex("UserId", "DeviceId");
|
||||
|
||||
b.ToTable("Devices");
|
||||
b.ToTable("Devices", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
|
||||
@@ -437,7 +441,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("DeviceId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DeviceOptions");
|
||||
b.ToTable("DeviceOptions", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
|
||||
@@ -550,7 +554,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
b.ToTable("Users", "jellyfin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Jellyfin.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.ModelConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// FluentAPI configuration for the ActivityLog entity.
|
||||
/// </summary>
|
||||
public class ActivityLogConfiguration : IEntityTypeConfiguration<ActivityLog>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Configure(EntityTypeBuilder<ActivityLog> builder)
|
||||
{
|
||||
builder.HasIndex(entity => entity.DateCreated);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy;
|
||||
using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy;
|
||||
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
||||
using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
|
||||
using Jellyfin.Api.Auth.LiveTvAccessPolicy;
|
||||
using Jellyfin.Api.Auth.LiveTvManagementPolicy;
|
||||
using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
|
||||
using Jellyfin.Api.Auth.LocalAccessPolicy;
|
||||
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
||||
@@ -66,6 +68,8 @@ namespace Jellyfin.Server.Extensions
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, LiveTvAccessHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, LiveTvManagementHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
|
||||
return serviceCollection.AddAuthorizationCore(options =>
|
||||
{
|
||||
@@ -167,6 +171,20 @@ namespace Jellyfin.Server.Extensions
|
||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||
policy.AddRequirements(new AnonymousLanAccessRequirement());
|
||||
});
|
||||
options.AddPolicy(
|
||||
Policies.LiveTvAccess,
|
||||
policy =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||
policy.AddRequirements(new LiveTvAccessRequirement());
|
||||
});
|
||||
options.AddPolicy(
|
||||
Policies.LiveTvManagement,
|
||||
policy =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||
policy.AddRequirements(new LiveTvManagementRequirement());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -434,11 +452,15 @@ namespace Jellyfin.Server.Extensions
|
||||
options.MapType<TranscodeReason>(() =>
|
||||
new OpenApiSchema
|
||||
{
|
||||
Type = "string",
|
||||
Enum = Enum.GetNames<TranscodeReason>()
|
||||
.Select(e => new OpenApiString(e))
|
||||
.Cast<IOpenApiAny>()
|
||||
.ToArray()
|
||||
Type = "array",
|
||||
Items = new OpenApiSchema
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Id = nameof(TranscodeReason),
|
||||
Type = ReferenceType.Schema,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Server.Migrations;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
@@ -8,6 +9,7 @@ using MediaBrowser.Model.ApiClient;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Session;
|
||||
using MediaBrowser.Model.SyncPlay;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
@@ -56,6 +58,15 @@ namespace Jellyfin.Server.Filters
|
||||
|
||||
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()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.9" />
|
||||
<PackageReference Include="prometheus-net" Version="6.0.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
|
||||
|
||||
@@ -19,41 +19,44 @@ namespace Jellyfin.Server.Middleware
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ResponseTimeMiddleware> _logger;
|
||||
|
||||
private readonly bool _enableWarning;
|
||||
private readonly long _warningThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ResponseTimeMiddleware"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">Next request delegate.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{ExceptionMiddleware}"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public ResponseTimeMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<ResponseTimeMiddleware> logger,
|
||||
IServerConfigurationManager serverConfigurationManager)
|
||||
ILogger<ResponseTimeMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
|
||||
_enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
|
||||
_warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke request.
|
||||
/// </summary>
|
||||
/// <param name="context">Request context.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Invoke(HttpContext context)
|
||||
public async Task Invoke(HttpContext context, IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
|
||||
var enableWarning = serverConfigurationManager.Configuration.EnableSlowResponseWarning;
|
||||
var warningThreshold = serverConfigurationManager.Configuration.SlowResponseThresholdMs;
|
||||
context.Response.OnStarting(() =>
|
||||
{
|
||||
watch.Stop();
|
||||
LogWarning(context, watch);
|
||||
if (enableWarning && watch.ElapsedMilliseconds > warningThreshold)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
|
||||
context.Request.GetDisplayUrl(),
|
||||
context.GetNormalizedRemoteIp(),
|
||||
watch.Elapsed,
|
||||
context.Response.StatusCode);
|
||||
}
|
||||
|
||||
var responseTimeForCompleteRequest = watch.ElapsedMilliseconds;
|
||||
context.Response.Headers[ResponseHeaderResponseTime] = responseTimeForCompleteRequest.ToString(CultureInfo.InvariantCulture);
|
||||
return Task.CompletedTask;
|
||||
@@ -62,18 +65,5 @@ namespace Jellyfin.Server.Middleware
|
||||
// Call the next delegate/middleware in the pipeline
|
||||
await this._next(context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void LogWarning(HttpContext context, Stopwatch watch)
|
||||
{
|
||||
if (_enableWarning && watch.ElapsedMilliseconds > _warningThreshold)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
|
||||
context.Request.GetDisplayUrl(),
|
||||
context.GetNormalizedRemoteIp(),
|
||||
watch.Elapsed,
|
||||
context.Response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ namespace Jellyfin.Server
|
||||
}
|
||||
}
|
||||
|
||||
appHost.Dispose();
|
||||
await appHost.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_restartOnShutdown)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.8.0</VersionPrefix>
|
||||
<VersionPrefix>10.8.13</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -170,6 +170,11 @@ namespace MediaBrowser.Common.Net
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
if (address.AddressFamily != AddressFamily)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var (altAddress, altPrefix) = NetworkAddressOf(address, PrefixLength);
|
||||
return NetworkAddress.Address.Equals(altAddress) && NetworkAddress.PrefixLength >= altPrefix;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ namespace MediaBrowser.Controller.ClientEvent
|
||||
{
|
||||
var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log";
|
||||
var logFilePath = Path.Combine(_applicationPaths.LogDirectoryPath, fileName);
|
||||
if (!Path.GetFullPath(logFilePath).StartsWith(_applicationPaths.LogDirectoryPath, StringComparison.Ordinal))
|
||||
{
|
||||
throw new ArgumentException("Path resolved to filename not in log directory");
|
||||
}
|
||||
|
||||
await using var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
||||
await fileContents.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
return fileName;
|
||||
|
||||
@@ -50,6 +50,14 @@ namespace MediaBrowser.Controller.Drawing
|
||||
/// <returns>BlurHash.</returns>
|
||||
string GetImageBlurHash(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the blurhash of the image.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the image file.</param>
|
||||
/// <param name="imageDimensions">The image dimensions.</param>
|
||||
/// <returns>BlurHash.</returns>
|
||||
string GetImageBlurHash(string path, ImageDimensions imageDimensions);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache tag.
|
||||
/// </summary>
|
||||
|
||||
@@ -169,8 +169,8 @@ namespace MediaBrowser.Controller.Entities.Audio
|
||||
|
||||
var childUpdateType = ItemUpdateType.None;
|
||||
|
||||
// Refresh songs
|
||||
foreach (var item in items)
|
||||
// Refresh songs only and not m3u files in album folder
|
||||
foreach (var item in items.OfType<Audio>())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
/// The supported image extensions.
|
||||
/// </summary>
|
||||
public static readonly string[] SupportedImageExtensions
|
||||
= new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" };
|
||||
= new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif" };
|
||||
|
||||
private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
|
||||
{
|
||||
@@ -1863,7 +1863,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
data.PlaybackPositionTicks = 0;
|
||||
}
|
||||
|
||||
data.LastPlayedDate = datePlayed ?? data.LastPlayedDate ?? DateTime.UtcNow;
|
||||
data.LastPlayedDate = datePlayed ?? DateTime.UtcNow;
|
||||
data.Played = true;
|
||||
|
||||
UserDataManager.SaveUserData(user.Id, this, data, UserDataSaveReason.TogglePlayed, CancellationToken.None);
|
||||
|
||||
@@ -892,29 +892,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
|
||||
{
|
||||
var ids = query.ItemIds;
|
||||
int size = items.Count;
|
||||
|
||||
// ids can potentially contain non-unique guids, but query result cannot,
|
||||
// so we include only first occurrence of each guid
|
||||
var positions = new Dictionary<Guid, int>(size);
|
||||
int index = 0;
|
||||
for (int i = 0; i < ids.Length; i++)
|
||||
{
|
||||
if (positions.TryAdd(ids[i], index))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
var newItems = new BaseItem[size];
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
newItems[positions[item.Id]] = item;
|
||||
}
|
||||
|
||||
return newItems;
|
||||
return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
|
||||
}
|
||||
|
||||
public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
|
||||
|
||||
@@ -205,6 +205,16 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
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 double? MinCriticRating { get; set; }
|
||||
|
||||
@@ -261,7 +261,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
DtoOptions = options
|
||||
};
|
||||
|
||||
if (!user.DisplayMissingEpisodes)
|
||||
if (user == null || !user.DisplayMissingEpisodes)
|
||||
{
|
||||
query.IsMissing = false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System.Net;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
@@ -74,9 +75,10 @@ namespace MediaBrowser.Controller
|
||||
/// <summary>
|
||||
/// Gets an URL that can be used to access the API over LAN.
|
||||
/// </summary>
|
||||
/// <param name="hostname">An optional hostname to use.</param>
|
||||
/// <param name="allowHttps">A value indicating whether to allow HTTPS.</param>
|
||||
/// <returns>The API URL.</returns>
|
||||
string GetApiUrlForLocalAccess(bool allowHttps = true);
|
||||
string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a local (LAN) URL that can be used to access the API.
|
||||
|
||||
@@ -570,5 +570,13 @@ namespace MediaBrowser.Controller.Library
|
||||
Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
|
||||
|
||||
BaseItem GetParentItem(Guid? parentId, Guid? userId);
|
||||
|
||||
/// <summary>
|
||||
/// Queue a library scan.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This exists so plugins can trigger a library scan.
|
||||
/// </remarks>
|
||||
void QueueLibraryScan();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.8.0</VersionPrefix>
|
||||
<VersionPrefix>10.8.13</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -75,6 +75,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// <value>The profile.</value>
|
||||
public string Profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video range type.
|
||||
/// </summary>
|
||||
/// <value>The video range type.</value>
|
||||
public string VideoRangeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the level.
|
||||
/// </summary>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -366,6 +366,28 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target video range type.
|
||||
/// </summary>
|
||||
public string TargetVideoRangeType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.VideoRangeType;
|
||||
}
|
||||
|
||||
var requestedRangeType = GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(requestedRangeType))
|
||||
{
|
||||
return requestedRangeType;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string TargetVideoCodecTag
|
||||
{
|
||||
get
|
||||
@@ -579,6 +601,26 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
public string[] GetRequestedRangeTypes(string codec)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(BaseRequest.VideoRangeType))
|
||||
{
|
||||
return BaseRequest.VideoRangeType.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var rangetype = BaseRequest.GetOption(codec, "rangetype");
|
||||
|
||||
if (!string.IsNullOrEmpty(rangetype))
|
||||
{
|
||||
return rangetype.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
public string GetRequestedLevel(string codec)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(BaseRequest.Level))
|
||||
|
||||
@@ -37,6 +37,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// <returns>The version of encoder.</returns>
|
||||
Version EncoderVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether p key pausing is supported.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if p key pausing is supported, <c>false</c> otherwise.</value>
|
||||
bool IsPkeyPauseSupported { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the configured Vaapi device is from AMD(radeonsi/r600 Mesa driver).
|
||||
/// </summary>
|
||||
|
||||
@@ -6,7 +6,8 @@ using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
@@ -37,11 +38,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// <summary>
|
||||
/// Gets the subtitle language encoding parameter.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="subtitleStream">The subtitle stream.</param>
|
||||
/// <param name="language">The language.</param>
|
||||
/// <param name="protocol">The protocol.</param>
|
||||
/// <param name="mediaSource">The media source.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken);
|
||||
Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
percent = 100.0 * currentMs / totalMs;
|
||||
|
||||
transcodingPosition = val;
|
||||
transcodingPosition = TimeSpan.FromMilliseconds(currentMs);
|
||||
}
|
||||
}
|
||||
else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
{
|
||||
public interface IWebSocketConnection
|
||||
public interface IWebSocketConnection : IAsyncDisposable, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when [closed].
|
||||
|
||||
@@ -34,8 +34,8 @@ namespace MediaBrowser.Controller.Providers
|
||||
|
||||
public bool IsReplacingImage(ImageType type)
|
||||
{
|
||||
return ImageRefreshMode == MetadataRefreshMode.FullRefresh &&
|
||||
(ReplaceAllImages || ReplaceImages.Contains(type));
|
||||
return ImageRefreshMode == MetadataRefreshMode.FullRefresh
|
||||
&& (ReplaceAllImages || ReplaceImages.Contains(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Session;
|
||||
@@ -17,7 +18,7 @@ namespace MediaBrowser.Controller.Session
|
||||
/// <summary>
|
||||
/// Class SessionInfo.
|
||||
/// </summary>
|
||||
public sealed class SessionInfo : IDisposable
|
||||
public sealed class SessionInfo : IAsyncDisposable, IDisposable
|
||||
{
|
||||
// 1 second
|
||||
private const long ProgressIncrement = 10000000;
|
||||
@@ -380,10 +381,28 @@ namespace MediaBrowser.Controller.Session
|
||||
{
|
||||
if (controller is IDisposable disposable)
|
||||
{
|
||||
_logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name);
|
||||
_logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name);
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
StopAutomaticProgress();
|
||||
|
||||
var controllers = SessionControllers.ToList();
|
||||
|
||||
foreach (var controller in controllers)
|
||||
{
|
||||
if (controller is IAsyncDisposable disposableAsync)
|
||||
{
|
||||
_logger.LogDebug("Disposing session controller asynchronously {TypeName}", disposableAsync.GetType().Name);
|
||||
await disposableAsync.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.MediaEncoding.Encoder;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -317,10 +318,10 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-dump_attachment:{1} {2} -i {0} -t 0 -f null null",
|
||||
"-dump_attachment:{1} \"{2}\" -i {0} -t 0 -f null null",
|
||||
inputPath,
|
||||
attachmentStreamIndex,
|
||||
outputPath);
|
||||
EncodingUtils.NormalizePath(outputPath));
|
||||
|
||||
int exitCode;
|
||||
|
||||
|
||||
@@ -25,11 +25,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
"mpeg2video",
|
||||
"mpeg4",
|
||||
"msmpeg4",
|
||||
"dts",
|
||||
"dca",
|
||||
"ac3",
|
||||
"aac",
|
||||
"mp3",
|
||||
"flac",
|
||||
"truehd",
|
||||
"h264_qsv",
|
||||
"hevc_qsv",
|
||||
"mpeg2_qsv",
|
||||
@@ -58,10 +59,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
"aac",
|
||||
"libfdk_aac",
|
||||
"ac3",
|
||||
"dca",
|
||||
"libmp3lame",
|
||||
"libopus",
|
||||
"libvorbis",
|
||||
"flac",
|
||||
"truehd",
|
||||
"srt",
|
||||
"h264_amf",
|
||||
"hevc_amf",
|
||||
@@ -100,6 +103,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
"scale_vaapi",
|
||||
"deinterlace_vaapi",
|
||||
"tonemap_vaapi",
|
||||
"procamp_vaapi",
|
||||
"overlay_vaapi",
|
||||
"hwupload_vaapi"
|
||||
};
|
||||
@@ -152,7 +156,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-version", false);
|
||||
output = GetProcessOutput(_encoderPath, "-version", false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -233,7 +237,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-version", false);
|
||||
output = GetProcessOutput(_encoderPath, "-version", false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -340,7 +344,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
try
|
||||
{
|
||||
var output = GetProcessOutput(_encoderPath, "-v verbose -hide_banner -init_hw_device vaapi=va:" + renderNodePath, true);
|
||||
var output = GetProcessOutput(_encoderPath, "-v verbose -hide_banner -init_hw_device vaapi=va:" + renderNodePath, true, null);
|
||||
return output.Contains(driverName, StringComparison.Ordinal);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -355,7 +359,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
string? output = null;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-hwaccels", false);
|
||||
output = GetProcessOutput(_encoderPath, "-hwaccels", false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -383,7 +387,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-h filter=" + filter, false);
|
||||
output = GetProcessOutput(_encoderPath, "-h filter=" + filter, false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -401,13 +405,34 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CheckSupportedRuntimeKey(string keyDesc)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyDesc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-hide_banner -f lavfi -i nullsrc=s=1x1:d=500 -f null -", true, "?");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error checking supported runtime key");
|
||||
return false;
|
||||
}
|
||||
|
||||
return output.Contains(keyDesc, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetCodecs(Codec codec)
|
||||
{
|
||||
string codecstr = codec == Codec.Encoder ? "encoders" : "decoders";
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-" + codecstr, false);
|
||||
output = GetProcessOutput(_encoderPath, "-" + codecstr, false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -438,7 +463,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-filters", false);
|
||||
output = GetProcessOutput(_encoderPath, "-filters", false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -476,7 +501,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return dict;
|
||||
}
|
||||
|
||||
private string GetProcessOutput(string path, string arguments, bool readStdErr)
|
||||
private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey)
|
||||
{
|
||||
using (var process = new Process()
|
||||
{
|
||||
@@ -486,6 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
UseShellExecute = false,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false,
|
||||
RedirectStandardInput = !string.IsNullOrEmpty(testKey),
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
}
|
||||
@@ -495,6 +521,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
process.Start();
|
||||
|
||||
if (!string.IsNullOrEmpty(testKey))
|
||||
{
|
||||
process.StandardInput.Write(testKey);
|
||||
}
|
||||
|
||||
return readStdErr ? process.StandardError.ReadToEnd() : process.StandardOutput.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string NormalizePath(string path)
|
||||
public static string NormalizePath(string path)
|
||||
{
|
||||
// Quotes are valid path characters in linux and they need to be escaped here with a leading \
|
||||
return path.Replace("\"", "\\\"", StringComparison.Ordinal);
|
||||
|
||||
@@ -67,6 +67,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
private List<string> _filters = new List<string>();
|
||||
private IDictionary<int, bool> _filtersWithOption = new Dictionary<int, bool>();
|
||||
|
||||
private bool _isPkeyPauseSupported = false;
|
||||
|
||||
private bool _isVaapiDeviceAmd = false;
|
||||
private bool _isVaapiDeviceInteliHD = false;
|
||||
private bool _isVaapiDeviceInteli965 = false;
|
||||
@@ -100,6 +102,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
public Version EncoderVersion => _ffmpegVersion;
|
||||
|
||||
public bool IsPkeyPauseSupported => _isPkeyPauseSupported;
|
||||
|
||||
public bool IsVaapiDeviceAmd => _isVaapiDeviceAmd;
|
||||
|
||||
public bool IsVaapiDeviceInteliHD => _isVaapiDeviceInteliHD;
|
||||
@@ -154,6 +158,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
_threads = EncodingHelper.GetNumberOfThreads(null, options, null);
|
||||
|
||||
_isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding");
|
||||
|
||||
// Check the Vaapi device vendor
|
||||
if (OperatingSystem.IsLinux()
|
||||
&& SupportsHwaccel("vaapi")
|
||||
@@ -376,15 +382,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
string analyzeDuration = string.Empty;
|
||||
string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
|
||||
{
|
||||
analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||
}
|
||||
else if (request.MediaSource.AnalyzeDurationMs > 0)
|
||||
if (request.MediaSource.AnalyzeDurationMs > 0)
|
||||
{
|
||||
analyzeDuration = "-analyzeduration " +
|
||||
(request.MediaSource.AnalyzeDurationMs * 1000).ToString();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
|
||||
{
|
||||
analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||
}
|
||||
|
||||
var forceEnableLogging = request.MediaSource.Protocol != MediaProtocol.File;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<PackageReference Include="libse" Version="3.6.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||
<PackageReference Include="UTF.Unknown" Version="2.5.0" />
|
||||
<PackageReference Include="UTF.Unknown" Version="2.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
|
||||
@@ -310,5 +310,12 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// <value>The color primaries.</value>
|
||||
[JsonPropertyName("color_primaries")]
|
||||
public string ColorPrimaries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the side_data_list.
|
||||
/// </summary>
|
||||
/// <value>The side_data_list.</value>
|
||||
[JsonPropertyName("side_data_list")]
|
||||
public IReadOnlyList<MediaStreamInfoSideData> SideDataList { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaStreamInfoSideData.
|
||||
/// </summary>
|
||||
public class MediaStreamInfoSideData
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the SideDataType.
|
||||
/// </summary>
|
||||
/// <value>The SideDataType.</value>
|
||||
[JsonPropertyName("side_data_type")]
|
||||
public string? SideDataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DvVersionMajor.
|
||||
/// </summary>
|
||||
/// <value>The DvVersionMajor.</value>
|
||||
[JsonPropertyName("dv_version_major")]
|
||||
public int? DvVersionMajor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DvVersionMinor.
|
||||
/// </summary>
|
||||
/// <value>The DvVersionMinor.</value>
|
||||
[JsonPropertyName("dv_version_minor")]
|
||||
public int? DvVersionMinor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DvProfile.
|
||||
/// </summary>
|
||||
/// <value>The DvProfile.</value>
|
||||
[JsonPropertyName("dv_profile")]
|
||||
public int? DvProfile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DvLevel.
|
||||
/// </summary>
|
||||
/// <value>The DvLevel.</value>
|
||||
[JsonPropertyName("dv_level")]
|
||||
public int? DvLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the RpuPresentFlag.
|
||||
/// </summary>
|
||||
/// <value>The RpuPresentFlag.</value>
|
||||
[JsonPropertyName("rpu_present_flag")]
|
||||
public int? RpuPresentFlag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ElPresentFlag.
|
||||
/// </summary>
|
||||
/// <value>The ElPresentFlag.</value>
|
||||
[JsonPropertyName("el_present_flag")]
|
||||
public int? ElPresentFlag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the BlPresentFlag.
|
||||
/// </summary>
|
||||
/// <value>The BlPresentFlag.</value>
|
||||
[JsonPropertyName("bl_present_flag")]
|
||||
public int? BlPresentFlag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DvBlSignalCompatibilityId.
|
||||
/// </summary>
|
||||
/// <value>The DvBlSignalCompatibilityId.</value>
|
||||
[JsonPropertyName("dv_bl_signal_compatibility_id")]
|
||||
public int? DvBlSignalCompatibilityId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -751,9 +751,11 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
|
||||
if (isAudio
|
||||
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
|
||||
&& (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
stream.Type = MediaStreamType.EmbeddedImage;
|
||||
}
|
||||
@@ -841,9 +843,35 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
stream.ColorPrimaries = streamInfo.ColorPrimaries;
|
||||
}
|
||||
|
||||
if (streamInfo.SideDataList != null)
|
||||
{
|
||||
foreach (var data in streamInfo.SideDataList)
|
||||
{
|
||||
// Parse Dolby Vision metadata from side_data
|
||||
if (string.Equals(data.SideDataType, "DOVI configuration record", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.DvVersionMajor = data.DvVersionMajor;
|
||||
stream.DvVersionMinor = data.DvVersionMinor;
|
||||
stream.DvProfile = data.DvProfile;
|
||||
stream.DvLevel = data.DvLevel;
|
||||
stream.RpuPresentFlag = data.RpuPresentFlag;
|
||||
stream.ElPresentFlag = data.ElPresentFlag;
|
||||
stream.BlPresentFlag = data.BlPresentFlag;
|
||||
stream.DvBlSignalCompatibilityId = data.DvBlSignalCompatibilityId;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (string.Equals(streamInfo.CodecType, "data", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Data;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Codec Type {CodecType} unknown. The stream (index: {Index}) will be ignored. Warning: Subsequential streams will have a wrong stream specifier!", streamInfo.CodecType, streamInfo.Index);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
54
MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs
Normal file
54
MediaBrowser.MediaEncoding/Subtitles/AssWriter.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// ASS subtitle writer.
|
||||
/// </summary>
|
||||
public class AssWriter : ISubtitleWriter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
{
|
||||
var trackEvents = info.TrackEvents;
|
||||
var timeFormat = @"hh\:mm\:ss\.ff";
|
||||
|
||||
// Write ASS header
|
||||
writer.WriteLine("[Script Info]");
|
||||
writer.WriteLine("Title: Jellyfin transcoded ASS subtitle");
|
||||
writer.WriteLine("ScriptType: v4.00+");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("[V4+ Styles]");
|
||||
writer.WriteLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding");
|
||||
writer.WriteLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H19333333,&H910E0807,0,0,0,0,100,100,0,0,0,1,0,2,10,10,10,1");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("[Events]");
|
||||
writer.WriteLine("Format: Layer, Start, End, Style, Text");
|
||||
|
||||
for (int i = 0; i < trackEvents.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var trackEvent = trackEvents[i];
|
||||
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase);
|
||||
|
||||
writer.WriteLine(
|
||||
"Dialogue: 0,{0},{1},Default,{2}",
|
||||
startTime,
|
||||
endTime,
|
||||
text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs
Normal file
54
MediaBrowser.MediaEncoding/Subtitles/SsaWriter.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// SSA subtitle writer.
|
||||
/// </summary>
|
||||
public class SsaWriter : ISubtitleWriter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
{
|
||||
var trackEvents = info.TrackEvents;
|
||||
var timeFormat = @"hh\:mm\:ss\.ff";
|
||||
|
||||
// Write SSA header
|
||||
writer.WriteLine("[Script Info]");
|
||||
writer.WriteLine("Title: Jellyfin transcoded SSA subtitle");
|
||||
writer.WriteLine("ScriptType: v4.00");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("[V4 Styles]");
|
||||
writer.WriteLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding");
|
||||
writer.WriteLine("Style: Default,Arial,20,&H00FFFFFF,&H00FFFFFF,&H19333333,&H19333333,0,0,0,1,0,2,10,10,10,0,1");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("[Events]");
|
||||
writer.WriteLine("Format: Layer, Start, End, Style, Text");
|
||||
|
||||
for (int i = 0; i < trackEvents.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var trackEvent = trackEvents[i];
|
||||
var startTime = TimeSpan.FromTicks(trackEvent.StartPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var endTime = TimeSpan.FromTicks(trackEvent.EndPositionTicks).ToString(timeFormat, CultureInfo.InvariantCulture);
|
||||
var text = Regex.Replace(trackEvent.Text, @"\n", "\\n", RegexOptions.IgnoreCase);
|
||||
|
||||
writer.WriteLine(
|
||||
"Dialogue: 0,{0},{1},Default,{2}",
|
||||
startTime,
|
||||
endTime,
|
||||
text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
// Convert
|
||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
|
||||
|
||||
await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
||||
await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
|
||||
}
|
||||
@@ -283,6 +283,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
|
||||
{
|
||||
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = new AssWriter();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(format))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(format));
|
||||
@@ -294,12 +300,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.SUBRIP, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = new SrtWriter();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = new SsaWriter();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = new VttWriter();
|
||||
@@ -339,13 +351,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <summary>
|
||||
/// Converts the text subtitle to SRT.
|
||||
/// </summary>
|
||||
/// <param name="inputPath">The input path.</param>
|
||||
/// <param name="language">The language.</param>
|
||||
/// <param name="subtitleStream">The subtitle stream.</param>
|
||||
/// <param name="mediaSource">The input mediaSource.</param>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task ConvertTextSubtitleToSrt(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
|
||||
private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var semaphore = GetLock(outputPath);
|
||||
|
||||
@@ -355,7 +366,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
await ConvertTextSubtitleToSrtInternal(inputPath, language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
||||
await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -367,8 +378,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <summary>
|
||||
/// Converts the text subtitle to SRT internal.
|
||||
/// </summary>
|
||||
/// <param name="inputPath">The input path.</param>
|
||||
/// <param name="language">The language.</param>
|
||||
/// <param name="subtitleStream">The subtitle stream.</param>
|
||||
/// <param name="mediaSource">The input mediaSource.</param>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
@@ -376,8 +386,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>.
|
||||
/// </exception>
|
||||
private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
|
||||
private async Task ConvertTextSubtitleToSrtInternal(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var inputPath = subtitleStream.Path;
|
||||
if (string.IsNullOrEmpty(inputPath))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(inputPath));
|
||||
@@ -390,7 +401,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)));
|
||||
|
||||
var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
|
||||
var encodingParam = await GetSubtitleFileCharacterSet(subtitleStream, subtitleStream.Language, mediaSource, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// FFmpeg automatically convert character encoding when it is UTF-16
|
||||
// If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event"
|
||||
@@ -408,18 +419,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
int exitCode;
|
||||
|
||||
using (var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
})
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
})
|
||||
{
|
||||
_logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
@@ -559,7 +570,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"",
|
||||
"-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
|
||||
inputPath,
|
||||
subtitleStreamIndex,
|
||||
outputCodec,
|
||||
@@ -568,18 +579,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
int exitCode;
|
||||
|
||||
using (var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = processArgs,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
})
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = processArgs,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
})
|
||||
{
|
||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
@@ -594,7 +605,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
throw;
|
||||
}
|
||||
|
||||
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false);
|
||||
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
@@ -717,9 +728,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken)
|
||||
public async Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
|
||||
var subtitleCodec = subtitleStream.Codec;
|
||||
var path = subtitleStream.Path;
|
||||
|
||||
if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec);
|
||||
await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
using (var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty;
|
||||
|
||||
@@ -742,12 +763,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
switch (protocol)
|
||||
{
|
||||
case MediaProtocol.Http:
|
||||
{
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(new Uri(path), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
{
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(new Uri(path), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
case MediaProtocol.File:
|
||||
return AsyncFile.OpenRead(path);
|
||||
|
||||
@@ -20,6 +20,11 @@ public class BrandingOptions
|
||||
/// <value>The custom CSS.</value>
|
||||
public string? CustomCss { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable the splashscreen.
|
||||
/// </summary>
|
||||
public bool SplashscreenEnabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the splashscreen location on disk.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,18 +21,21 @@ namespace MediaBrowser.Model.Configuration
|
||||
EnableTonemapping = false;
|
||||
EnableVppTonemapping = false;
|
||||
TonemappingAlgorithm = "bt2390";
|
||||
TonemappingMode = "auto";
|
||||
TonemappingRange = "auto";
|
||||
TonemappingDesat = 0;
|
||||
TonemappingThreshold = 0.8;
|
||||
TonemappingPeak = 100;
|
||||
TonemappingParam = 0;
|
||||
VppTonemappingBrightness = 16;
|
||||
VppTonemappingContrast = 1;
|
||||
H264Crf = 23;
|
||||
H265Crf = 28;
|
||||
DeinterlaceDoubleRate = false;
|
||||
DeinterlaceMethod = "yadif";
|
||||
EnableDecodingColorDepth10Hevc = true;
|
||||
EnableDecodingColorDepth10Vp9 = true;
|
||||
EnableEnhancedNvdecDecoder = false;
|
||||
// Enhanced Nvdec or system native decoder is required for DoVi to SDR tone-mapping.
|
||||
EnableEnhancedNvdecDecoder = true;
|
||||
PreferSystemNativeHwDecoder = true;
|
||||
EnableIntelLowPowerH264HwEncoder = false;
|
||||
EnableIntelLowPowerHevcHwEncoder = false;
|
||||
@@ -79,16 +82,20 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
public string TonemappingAlgorithm { get; set; }
|
||||
|
||||
public string TonemappingMode { get; set; }
|
||||
|
||||
public string TonemappingRange { get; set; }
|
||||
|
||||
public double TonemappingDesat { get; set; }
|
||||
|
||||
public double TonemappingThreshold { get; set; }
|
||||
|
||||
public double TonemappingPeak { get; set; }
|
||||
|
||||
public double TonemappingParam { get; set; }
|
||||
|
||||
public double VppTonemappingBrightness { get; set; }
|
||||
|
||||
public double VppTonemappingContrast { get; set; }
|
||||
|
||||
public int H264Crf { get; set; }
|
||||
|
||||
public int H265Crf { get; set; }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user