mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-12 12:03:02 +03:00
Compare commits
200 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 | ||
|
|
3bd2cc9860 | ||
|
|
feb035b9e0 |
@@ -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
|
||||
|
||||
|
||||
@@ -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.2</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>
|
||||
|
||||
@@ -83,7 +83,6 @@ using MediaBrowser.Controller.SyncPlay;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.LocalMetadata.Savers;
|
||||
using MediaBrowser.MediaEncoding.BdInfo;
|
||||
using MediaBrowser.MediaEncoding.Subtitles;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
@@ -635,8 +634,7 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton<IAuthService, AuthService>();
|
||||
serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
|
||||
serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
|
||||
serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
|
||||
|
||||
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
|
||||
@@ -1090,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/>
|
||||
|
||||
@@ -3541,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");
|
||||
|
||||
@@ -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.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.3" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
|
||||
<PackageReference Include="sharpcompress" Version="0.32.1" />
|
||||
<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>
|
||||
|
||||
@@ -2766,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
|
||||
{
|
||||
@@ -2777,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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -1662,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>
|
||||
@@ -1693,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);
|
||||
}
|
||||
@@ -1712,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";
|
||||
}
|
||||
@@ -1745,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);
|
||||
}
|
||||
@@ -1841,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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -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))
|
||||
@@ -217,10 +229,25 @@ namespace Jellyfin.Api.Helpers
|
||||
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
||||
|
||||
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
|
||||
var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
|
||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||
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>
|
||||
|
||||
@@ -182,12 +182,18 @@ namespace Jellyfin.Api.Helpers
|
||||
: 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)
|
||||
|
||||
@@ -654,7 +654,7 @@ namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
if (EnableThrottling(state))
|
||||
{
|
||||
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem);
|
||||
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder);
|
||||
transcodingJob.TranscodingThrottler.Start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.7" />
|
||||
<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)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.8.2</VersionPrefix>
|
||||
<VersionPrefix>10.8.13</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BlurHashSharp" Version="1.2.0" />
|
||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.79" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.79" />
|
||||
<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>
|
||||
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
||||
<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.7">
|
||||
<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.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.7" />
|
||||
<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" />
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.8.2</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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -258,10 +258,14 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||
DtoOptions = options,
|
||||
IsMissing = user?.DisplayMissingEpisodes
|
||||
DtoOptions = options
|
||||
};
|
||||
|
||||
if (user == null || !user.DisplayMissingEpisodes)
|
||||
{
|
||||
query.IsMissing = false;
|
||||
}
|
||||
|
||||
var allItems = LibraryManager.GetItemList(query);
|
||||
|
||||
var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.8.2</VersionPrefix>
|
||||
<VersionPrefix>10.8.13</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -25,6 +25,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
public class EncodingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The codec validation regex.
|
||||
/// </summary>
|
||||
public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
|
||||
|
||||
private const string QsvAlias = "qs";
|
||||
private const string VaapiAlias = "va";
|
||||
private const string D3d11vaAlias = "dx11";
|
||||
@@ -35,7 +40,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly Version _minKernelVersioni915Hang = new Version(5, 18);
|
||||
|
||||
private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
|
||||
|
||||
// i915 hang was fixed by linux 6.2 (3f882f2)
|
||||
private readonly Version _minKerneli915Hang = new Version(5, 18);
|
||||
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
|
||||
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
|
||||
|
||||
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
|
||||
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
|
||||
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
|
||||
|
||||
private static readonly string[] _videoProfilesH264 = new[]
|
||||
{
|
||||
@@ -55,6 +70,16 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
"Main10"
|
||||
};
|
||||
|
||||
public static readonly string[] LosslessAudioCodecs = new string[]
|
||||
{
|
||||
"alac",
|
||||
"ape",
|
||||
"flac",
|
||||
"mlp",
|
||||
"truehd",
|
||||
"wavpack"
|
||||
};
|
||||
|
||||
public EncodingHelper(
|
||||
IApplicationPaths appPaths,
|
||||
IMediaEncoder mediaEncoder,
|
||||
@@ -235,7 +260,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return "libtheora";
|
||||
}
|
||||
|
||||
return codec.ToLowerInvariant();
|
||||
if (_validationRegex.IsMatch(codec))
|
||||
{
|
||||
return codec.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
return "copy";
|
||||
@@ -258,7 +286,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public static string GetInputFormat(string container)
|
||||
{
|
||||
if (string.IsNullOrEmpty(container))
|
||||
if (string.IsNullOrEmpty(container) || !_validationRegex.IsMatch(container))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -505,6 +533,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
var codec = state.OutputAudioCodec;
|
||||
|
||||
if (!_validationRegex.IsMatch(codec))
|
||||
{
|
||||
codec = "aac";
|
||||
}
|
||||
|
||||
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Use libfdk_aac for better audio quality if using custom build of FFmpeg which has fdk_aac support
|
||||
@@ -541,6 +574,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return "flac";
|
||||
}
|
||||
|
||||
if (string.Equals(codec, "dts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "dca";
|
||||
}
|
||||
|
||||
return codec.ToLowerInvariant();
|
||||
}
|
||||
|
||||
@@ -604,14 +642,20 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string alias)
|
||||
{
|
||||
alias ??= VaapiAlias;
|
||||
renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
|
||||
var options = string.IsNullOrEmpty(driver)
|
||||
? renderNodePath
|
||||
: ",driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver);
|
||||
|
||||
// 'renderNodePath' has higher priority than 'kernelDriver'
|
||||
var driverOpts = string.IsNullOrEmpty(renderNodePath)
|
||||
? (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver)
|
||||
: renderNodePath;
|
||||
|
||||
// 'driver' behaves similarly to env LIBVA_DRIVER_NAME
|
||||
driverOpts += string.IsNullOrEmpty(driver) ? string.Empty : ",driver=" + driver;
|
||||
|
||||
var options = string.IsNullOrEmpty(driverOpts) ? string.Empty : ":" + driverOpts;
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -init_hw_device vaapi={0}:{1}",
|
||||
" -init_hw_device vaapi={0}{1}",
|
||||
alias,
|
||||
options);
|
||||
}
|
||||
@@ -643,9 +687,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public string GetGraphicalSubCanvasSize(EncodingJobInfo state)
|
||||
{
|
||||
// DVBSUB and DVDSUB use the fixed canvas size 720x576
|
||||
if (state.SubtitleStream != null
|
||||
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
|
||||
&& !state.SubtitleStream.IsTextSubtitleStream)
|
||||
&& !state.SubtitleStream.IsTextSubtitleStream
|
||||
&& !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(state.SubtitleStream.Codec, "DVDSUB", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var inW = state.VideoStream?.Width;
|
||||
var inH = state.VideoStream?.Height;
|
||||
@@ -713,14 +760,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (_mediaEncoder.IsVaapiDeviceInteliHD)
|
||||
{
|
||||
args.Append(GetVaapiDeviceArgs(null, "iHD", null, VaapiAlias));
|
||||
args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, VaapiAlias));
|
||||
}
|
||||
else if (_mediaEncoder.IsVaapiDeviceInteli965)
|
||||
{
|
||||
// Only override i965 since it has lower priority than iHD in libva lookup.
|
||||
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965");
|
||||
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965");
|
||||
args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias));
|
||||
args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, VaapiAlias));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1152,16 +1199,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (state.SubtitleStream.IsExternal)
|
||||
{
|
||||
var subtitlePath = state.SubtitleStream.Path;
|
||||
var charsetParam = string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
|
||||
{
|
||||
var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet(
|
||||
subtitlePath,
|
||||
state.SubtitleStream.Language,
|
||||
state.MediaSource.Protocol,
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
state.SubtitleStream,
|
||||
state.SubtitleStream.Language,
|
||||
state.MediaSource,
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
if (!string.IsNullOrEmpty(charenc))
|
||||
{
|
||||
@@ -1173,7 +1219,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"subtitles=f='{0}'{1}{2}{3}{4}{5}",
|
||||
_mediaEncoder.EscapeSubtitleFilterPath(subtitlePath),
|
||||
_mediaEncoder.EscapeSubtitleFilterPath(state.SubtitleStream.Path),
|
||||
charsetParam,
|
||||
alphaParam,
|
||||
sub2videoParam,
|
||||
@@ -1288,6 +1334,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
args += keyFrameArg + gopArg;
|
||||
}
|
||||
|
||||
// global_header produced by AMD VA-API encoder causes non-playable fMP4 on iOS
|
||||
if (codec.Contains("vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
&& _mediaEncoder.IsVaapiDeviceAmd)
|
||||
{
|
||||
args += " -flags:v -global_header";
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -1310,7 +1363,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// which will reduce overhead in performance intensive tasks such as 4k transcoding and tonemapping.
|
||||
var intelLowPowerHwEncoding = false;
|
||||
|
||||
// Workaround for linux 5.18+ i915 hang at cost of performance.
|
||||
// Workaround for linux 5.18 to 6.1.3 i915 hang at cost of performance.
|
||||
// https://github.com/intel/media-driver/issues/1456
|
||||
var enableWaFori915Hang = false;
|
||||
|
||||
@@ -1329,18 +1382,25 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
else if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (OperatingSystem.IsLinux() && Environment.OSVersion.Version >= _minKernelVersioni915Hang)
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
var vidDecoder = GetHardwareVideoDecoder(state, encodingOptions) ?? string.Empty;
|
||||
var isIntelDecoder = vidDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
|
||||
var doOclTonemap = _mediaEncoder.SupportsHwaccel("qsv")
|
||||
&& IsVaapiSupported(state)
|
||||
&& IsOpenclFullSupported()
|
||||
&& !IsVaapiVppTonemapAvailable(state, encodingOptions)
|
||||
&& IsHwTonemapAvailable(state, encodingOptions);
|
||||
var ver = Environment.OSVersion.Version;
|
||||
var isFixedKernel60 = ver.Major == 6 && ver.Minor == 0 && ver >= _minFixedKernel60i915Hang;
|
||||
var isUnaffectedKernel = ver < _minKerneli915Hang || ver > _maxKerneli915Hang;
|
||||
|
||||
enableWaFori915Hang = isIntelDecoder && doOclTonemap;
|
||||
if (!(isUnaffectedKernel || isFixedKernel60))
|
||||
{
|
||||
var vidDecoder = GetHardwareVideoDecoder(state, encodingOptions) ?? string.Empty;
|
||||
var isIntelDecoder = vidDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
|
||||
var doOclTonemap = _mediaEncoder.SupportsHwaccel("qsv")
|
||||
&& IsVaapiSupported(state)
|
||||
&& IsOpenclFullSupported()
|
||||
&& !IsVaapiVppTonemapAvailable(state, encodingOptions)
|
||||
&& IsHwTonemapAvailable(state, encodingOptions);
|
||||
|
||||
enableWaFori915Hang = isIntelDecoder && doOclTonemap;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -1416,7 +1476,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
param += " -preset 7";
|
||||
}
|
||||
|
||||
param += " -look_ahead 0";
|
||||
// Only h264_qsv has look_ahead option
|
||||
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param += " -look_ahead 0";
|
||||
}
|
||||
}
|
||||
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|
||||
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
|
||||
@@ -1427,11 +1491,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
param += " -preset p7";
|
||||
break;
|
||||
|
||||
case "slow":
|
||||
case "slower":
|
||||
param += " -preset p6";
|
||||
break;
|
||||
|
||||
case "slower":
|
||||
case "slow":
|
||||
param += " -preset p5";
|
||||
break;
|
||||
|
||||
@@ -1454,7 +1518,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
break;
|
||||
|
||||
default:
|
||||
param += " -preset p4";
|
||||
param += " -preset p1";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1464,8 +1528,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
switch (encodingOptions.EncoderPreset)
|
||||
{
|
||||
case "veryslow":
|
||||
case "slow":
|
||||
case "slower":
|
||||
case "slow":
|
||||
param += " -quality quality";
|
||||
break;
|
||||
|
||||
@@ -1938,9 +2002,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
// Video bitrate must fall within requested value
|
||||
// Audio bitrate must fall within requested value
|
||||
if (request.AudioBitRate.HasValue
|
||||
&& audioStream.BitDepth.HasValue
|
||||
&& audioStream.BitRate.HasValue
|
||||
&& audioStream.BitRate.Value > request.AudioBitRate.Value)
|
||||
{
|
||||
return false;
|
||||
@@ -2004,14 +2068,20 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
private static double GetVideoBitrateScaleFactor(string codec)
|
||||
{
|
||||
// hevc & vp9 - 40% more efficient than h.264
|
||||
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return .6;
|
||||
}
|
||||
|
||||
// av1 - 50% more efficient than h.264
|
||||
if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return .5;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -2019,7 +2089,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec);
|
||||
var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
|
||||
var scaleFactor = outputScaleFactor / inputScaleFactor;
|
||||
|
||||
// Don't scale the real bitrate lower than the requested bitrate
|
||||
var scaleFactor = Math.Max(outputScaleFactor / inputScaleFactor, 1);
|
||||
|
||||
if (bitrate <= 500000)
|
||||
{
|
||||
@@ -2041,56 +2113,55 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return Convert.ToInt32(scaleFactor * bitrate);
|
||||
}
|
||||
|
||||
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
|
||||
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream, int? outputAudioChannels)
|
||||
{
|
||||
return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
|
||||
return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream, outputAudioChannels);
|
||||
}
|
||||
|
||||
public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
|
||||
public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream, int? outputAudioChannels)
|
||||
{
|
||||
if (audioStream == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec))
|
||||
var inputChannels = audioStream.Channels ?? 0;
|
||||
var outputChannels = outputAudioChannels ?? 0;
|
||||
var bitrate = audioBitRate ?? int.MaxValue;
|
||||
|
||||
if (string.IsNullOrEmpty(audioCodec)
|
||||
|| string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Math.Min(384000, audioBitRate.Value);
|
||||
return (inputChannels, outputChannels) switch
|
||||
{
|
||||
(>= 6, >= 6 or 0) => Math.Min(640000, bitrate),
|
||||
(> 0, > 0) => Math.Min(outputChannels * 128000, bitrate),
|
||||
(> 0, _) => Math.Min(inputChannels * 128000, bitrate),
|
||||
(_, _) => Math.Min(384000, bitrate)
|
||||
};
|
||||
}
|
||||
|
||||
if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec))
|
||||
if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
|
||||
return (inputChannels, outputChannels) switch
|
||||
{
|
||||
if ((audioStream.Channels ?? 0) >= 6)
|
||||
{
|
||||
return Math.Min(640000, audioBitRate.Value);
|
||||
}
|
||||
|
||||
return Math.Min(384000, audioBitRate.Value);
|
||||
}
|
||||
|
||||
if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if ((audioStream.Channels ?? 0) >= 6)
|
||||
{
|
||||
return Math.Min(3584000, audioBitRate.Value);
|
||||
}
|
||||
|
||||
return Math.Min(1536000, audioBitRate.Value);
|
||||
}
|
||||
(>= 6, >= 6 or 0) => Math.Min(768000, bitrate),
|
||||
(> 0, > 0) => Math.Min(outputChannels * 136000, bitrate),
|
||||
(> 0, _) => Math.Min(inputChannels * 136000, bitrate),
|
||||
(_, _) => Math.Min(672000, bitrate)
|
||||
};
|
||||
}
|
||||
|
||||
// Empty bitrate area is not allow on iOS
|
||||
// Default audio bitrate to 128K if it is not being requested
|
||||
// Default audio bitrate to 128K per channel if we don't have codec specific defaults
|
||||
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
|
||||
return 128000;
|
||||
return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 2);
|
||||
}
|
||||
|
||||
public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions)
|
||||
@@ -2383,6 +2454,30 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return args;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the negative map args by filters.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="videoProcessFilters">The videoProcessFilters.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public string GetNegativeMapArgsByFilters(EncodingJobInfo state, string videoProcessFilters)
|
||||
{
|
||||
string args = string.Empty;
|
||||
|
||||
// http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1
|
||||
if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal))
|
||||
{
|
||||
int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
|
||||
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-map -0:{0} ",
|
||||
videoStreamIndex);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines which stream will be used for playback.
|
||||
/// </summary>
|
||||
@@ -2753,7 +2848,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public static string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat)
|
||||
public string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hwTonemapSuffix))
|
||||
{
|
||||
@@ -2764,7 +2859,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (hwTonemapSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += ",procamp_vaapi=b={2}:c={3}:extra_hw_frames=16";
|
||||
args = "procamp_vaapi=b={2}:c={3}," + args + ":extra_hw_frames=32";
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
args,
|
||||
@@ -2777,14 +2873,24 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
args += ":tonemap={2}:peak={3}:desat={4}";
|
||||
|
||||
if (options.TonemappingParam != 0)
|
||||
if (string.Equals(options.TonemappingMode, "max", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(options.TonemappingMode, "rgb", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += ":param={5}";
|
||||
if (_mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode)
|
||||
{
|
||||
args += ":tonemap_mode={5}";
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
|
||||
if (options.TonemappingParam != 0)
|
||||
{
|
||||
args += ":range={6}";
|
||||
args += ":param={6}";
|
||||
}
|
||||
|
||||
if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += ":range={7}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2796,6 +2902,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
options.TonemappingAlgorithm,
|
||||
options.TonemappingPeak,
|
||||
options.TonemappingDesat,
|
||||
options.TonemappingMode,
|
||||
options.TonemappingParam,
|
||||
options.TonemappingRange);
|
||||
}
|
||||
@@ -2876,8 +2983,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
else if (hasGraphicalSubs)
|
||||
{
|
||||
// [0:s]scale=expr
|
||||
var subSwScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
|
||||
// [0:s]scale=s=1280x720
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
|
||||
}
|
||||
@@ -3063,9 +3170,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
|
||||
}
|
||||
@@ -3202,7 +3307,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
// OUTPUT nv12 surface(memory)
|
||||
// prefer hwmap to hwdownload on opencl.
|
||||
var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap";
|
||||
var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap=mode=read";
|
||||
mainFilters.Add(hwTransferFilter);
|
||||
mainFilters.Add("format=nv12");
|
||||
}
|
||||
@@ -3265,9 +3370,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
|
||||
}
|
||||
@@ -3441,7 +3544,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// OUTPUT nv12 surface(memory)
|
||||
// prefer hwmap to hwdownload on opencl.
|
||||
// qsv hwmap is not fully implemented for the time being.
|
||||
mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload");
|
||||
mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload");
|
||||
mainFilters.Add("format=nv12");
|
||||
}
|
||||
|
||||
@@ -3513,9 +3616,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
|
||||
}
|
||||
@@ -3601,6 +3702,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
var outFormat = doTonemap ? string.Empty : "nv12";
|
||||
var hwScaleFilter = GetHwScaleFilter(isVaapiDecoder ? "vaapi" : "qsv", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
|
||||
// allocate extra pool sizes for vaapi vpp
|
||||
if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder)
|
||||
{
|
||||
hwScaleFilter += ":extra_hw_frames=24";
|
||||
}
|
||||
|
||||
// hw scale
|
||||
mainFilters.Add(hwScaleFilter);
|
||||
}
|
||||
@@ -3647,7 +3755,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// OUTPUT nv12 surface(memory)
|
||||
// prefer hwmap to hwdownload on opencl/vaapi.
|
||||
// qsv hwmap is not fully implemented for the time being.
|
||||
mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload");
|
||||
mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload");
|
||||
mainFilters.Add("format=nv12");
|
||||
}
|
||||
|
||||
@@ -3724,9 +3832,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
|
||||
}
|
||||
@@ -3866,6 +3972,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
var outFormat = doTonemap ? string.Empty : "nv12";
|
||||
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
|
||||
// allocate extra pool sizes for vaapi vpp
|
||||
if (!string.IsNullOrEmpty(hwScaleFilter))
|
||||
{
|
||||
hwScaleFilter += ":extra_hw_frames=24";
|
||||
}
|
||||
|
||||
// hw scale
|
||||
mainFilters.Add(hwScaleFilter);
|
||||
}
|
||||
@@ -3907,7 +4020,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
// OUTPUT nv12 surface(memory)
|
||||
// prefer hwmap to hwdownload on opencl/vaapi.
|
||||
mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap");
|
||||
mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap=mode=read");
|
||||
mainFilters.Add("format=nv12");
|
||||
}
|
||||
|
||||
@@ -3973,9 +4086,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
|
||||
|
||||
@@ -4062,6 +4173,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
outFormat = doOclTonemap ? string.Empty : "nv12";
|
||||
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
|
||||
// allocate extra pool sizes for vaapi vpp
|
||||
if (!string.IsNullOrEmpty(hwScaleFilter))
|
||||
{
|
||||
hwScaleFilter += ":extra_hw_frames=24";
|
||||
}
|
||||
|
||||
// hw scale
|
||||
mainFilters.Add(hwScaleFilter);
|
||||
}
|
||||
@@ -4150,9 +4268,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
|
||||
|
||||
@@ -4368,7 +4484,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
// HWA decoders can handle both video files and video folders.
|
||||
var videoType = mediaSource.VideoType;
|
||||
var videoType = state.VideoType;
|
||||
if (videoType != VideoType.VideoFile
|
||||
&& videoType != VideoType.Iso
|
||||
&& videoType != VideoType.Dvd
|
||||
@@ -4509,8 +4625,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox");
|
||||
var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var ffmpegVersion = _mediaEncoder.EncoderVersion;
|
||||
|
||||
// Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used.
|
||||
var isAv1 = string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
|
||||
var isAv1 = ffmpegVersion < _minFFmpegImplictHwaccel
|
||||
&& string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels.
|
||||
var profileMismatch = string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(state.VideoStream?.Profile, "baseline", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Disable the extra internal copy in nvdec. We already handle it in filter chain.
|
||||
var nvdecNoInternalCopy = ffmpegVersion >= _minFFmpegHwaUnsafeOutput;
|
||||
|
||||
if (bitDepth == 10 && isCodecAvailable)
|
||||
{
|
||||
@@ -4536,14 +4662,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (isVaapiSupported && isCodecAvailable)
|
||||
{
|
||||
return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
|
||||
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
}
|
||||
|
||||
if (isD3d11Supported && isCodecAvailable)
|
||||
{
|
||||
// set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock.
|
||||
// on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11.
|
||||
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
|
||||
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 2" + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -4563,7 +4689,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
if (options.EnableEnhancedNvdecDecoder)
|
||||
{
|
||||
// set -threads 1 to nvdec decoder explicitly since it doesn't implement threading support.
|
||||
return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty)
|
||||
+ (nvdecNoInternalCopy ? " -hwaccel_flags +unsafe_output" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -4578,7 +4705,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (isD3d11Supported && isCodecAvailable)
|
||||
{
|
||||
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
|
||||
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4587,9 +4715,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
&& isVaapiSupported
|
||||
&& isCodecAvailable)
|
||||
{
|
||||
return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
|
||||
+ (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
|
||||
}
|
||||
|
||||
// Apple videotoolbox
|
||||
if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)
|
||||
&& isVideotoolboxSupported
|
||||
&& isCodecAvailable)
|
||||
@@ -4977,14 +5107,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// The default value of -probesize is more than enough, so leave it as is.
|
||||
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
|
||||
{
|
||||
analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||
}
|
||||
else if (state.MediaSource.AnalyzeDurationMs.HasValue)
|
||||
if (state.MediaSource.AnalyzeDurationMs > 0)
|
||||
{
|
||||
analyzeDurationArgument = "-analyzeduration " + (state.MediaSource.AnalyzeDurationMs.Value * 1000).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
|
||||
{
|
||||
analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(analyzeDurationArgument))
|
||||
{
|
||||
@@ -5205,15 +5335,23 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return;
|
||||
}
|
||||
|
||||
var inputChannels = audioStream == null ? 6 : audioStream.Channels ?? 6;
|
||||
var inputChannels = audioStream is null ? 6 : audioStream.Channels ?? 6;
|
||||
var shiftAudioCodecs = new List<string>();
|
||||
if (inputChannels >= 6)
|
||||
{
|
||||
return;
|
||||
// DTS and TrueHD are not supported by HLS
|
||||
// Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
|
||||
shiftAudioCodecs.Add("dca");
|
||||
shiftAudioCodecs.Add("truehd");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Transcoding to 2ch ac3 or eac3 almost always causes a playback failure
|
||||
// Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
|
||||
shiftAudioCodecs.Add("ac3");
|
||||
shiftAudioCodecs.Add("eac3");
|
||||
}
|
||||
|
||||
// Transcoding to 2ch ac3 almost always causes a playback failure
|
||||
// Keep it in the supported codecs list, but shift it to the end of the list so that if transcoding happens, another codec is used
|
||||
var shiftAudioCodecs = new[] { "ac3", "eac3" };
|
||||
if (audioCodecs.All(i => shiftAudioCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return;
|
||||
@@ -5390,7 +5528,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// video processing filters.
|
||||
var videoProcessParam = GetVideoProcessingFilterParam(state, encodingOptions, videoCodec);
|
||||
|
||||
args += videoProcessParam;
|
||||
var negativeMapArgs = GetNegativeMapArgsByFilters(state, videoProcessParam);
|
||||
|
||||
args = negativeMapArgs + args + videoProcessParam;
|
||||
|
||||
hasCopyTs = videoProcessParam.Contains("copyts", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
@@ -5455,7 +5595,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
var bitrate = state.OutputAudioBitrate;
|
||||
|
||||
if (bitrate.HasValue)
|
||||
if (bitrate.HasValue && !LosslessAudioCodecs.Contains(codec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -5475,8 +5615,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var audioTranscodeParams = new List<string>();
|
||||
|
||||
var bitrate = state.OutputAudioBitrate;
|
||||
var channels = state.OutputAudioChannels;
|
||||
var outputCodec = state.OutputAudioCodec;
|
||||
|
||||
if (bitrate.HasValue)
|
||||
if (bitrate.HasValue && !LosslessAudioCodecs.Contains(outputCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
@@ -5486,7 +5628,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.IsNullOrEmpty(outputCodec))
|
||||
{
|
||||
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
|
||||
}
|
||||
|
||||
if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// opus only supports specific sampling rates
|
||||
var sampleRate = state.OutputAudioSampleRate;
|
||||
@@ -5536,7 +5683,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return index;
|
||||
}
|
||||
|
||||
if (string.Equals(currentMediaStream.Path, streamToFind.Path, StringComparison.Ordinal))
|
||||
if (string.Equals(currentMediaStream.Path, streamToFind.Path, StringComparison.Ordinal))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -153,7 +156,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-version", false);
|
||||
output = GetProcessOutput(_encoderPath, "-version", false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -234,7 +237,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-version", false);
|
||||
output = GetProcessOutput(_encoderPath, "-version", false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -341,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)
|
||||
@@ -356,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)
|
||||
{
|
||||
@@ -384,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)
|
||||
{
|
||||
@@ -402,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)
|
||||
{
|
||||
@@ -439,7 +463,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
string output;
|
||||
try
|
||||
{
|
||||
output = GetProcessOutput(_encoderPath, "-filters", false);
|
||||
output = GetProcessOutput(_encoderPath, "-filters", false, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -477,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()
|
||||
{
|
||||
@@ -487,6 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
UseShellExecute = false,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false,
|
||||
RedirectStandardInput = !string.IsNullOrEmpty(testKey),
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
}
|
||||
@@ -496,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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -863,8 +865,13 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
19
MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
Normal file
19
MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Nikse.SubtitleEdit.Core.SubtitleFormats;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// Advanced SubStation Alpha subtitle parser.
|
||||
/// </summary>
|
||||
public class AssParser : SubtitleEditParser<AdvancedSubStationAlpha>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AssParser"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public AssParser(ILogger logger) : base(logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
@@ -11,15 +12,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// Parses the specified stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="fileExtension">The file extension.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>SubtitleTrackInfo.</returns>
|
||||
SubtitleTrackInfo Parse(Stream stream, string fileExtension);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the file extension is supported by the parser.
|
||||
/// </summary>
|
||||
/// <param name="fileExtension">The file extension.</param>
|
||||
/// <returns>A value indicating whether the file extension is supported.</returns>
|
||||
bool SupportsFileExtension(string fileExtension);
|
||||
SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
19
MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
Normal file
19
MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Nikse.SubtitleEdit.Core.SubtitleFormats;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// SubRip subtitle parser.
|
||||
/// </summary>
|
||||
public class SrtParser : SubtitleEditParser<SubRip>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SrtParser"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public SrtParser(ILogger logger) : base(logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
19
MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
Normal file
19
MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Nikse.SubtitleEdit.Core.SubtitleFormats;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// SubStation Alpha subtitle parser.
|
||||
/// </summary>
|
||||
public class SsaParser : SubtitleEditParser<SubStationAlpha>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SsaParser"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public SsaParser(ILogger logger) : base(logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Nikse.SubtitleEdit.Core.Common;
|
||||
using Nikse.SubtitleEdit.Core.SubtitleFormats;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
@@ -16,57 +14,31 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <summary>
|
||||
/// SubStation Alpha subtitle parser.
|
||||
/// </summary>
|
||||
public class SubtitleEditParser : ISubtitleParser
|
||||
/// <typeparam name="T">The <see cref="SubtitleFormat" />.</typeparam>
|
||||
public abstract class SubtitleEditParser<T> : ISubtitleParser
|
||||
where T : SubtitleFormat, new()
|
||||
{
|
||||
private readonly ILogger<SubtitleEditParser> _logger;
|
||||
private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
|
||||
/// Initializes a new instance of the <see cref="SubtitleEditParser{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
|
||||
protected SubtitleEditParser(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_subtitleFormats = GetSubtitleFormats()
|
||||
.Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension))
|
||||
.GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SubtitleTrackInfo Parse(Stream stream, string fileExtension)
|
||||
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var subtitle = new Subtitle();
|
||||
var subRip = new T();
|
||||
var lines = stream.ReadAllLines().ToList();
|
||||
|
||||
if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats))
|
||||
subRip.LoadSubtitle(subtitle, lines, "untitled");
|
||||
if (subRip.ErrorCount > 0)
|
||||
{
|
||||
throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
|
||||
}
|
||||
|
||||
foreach (var subtitleFormat in subtitleFormats)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
|
||||
fileExtension,
|
||||
subtitleFormat.Name);
|
||||
subtitleFormat.LoadSubtitle(subtitle, lines, fileExtension);
|
||||
if (subtitleFormat.ErrorCount == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.LogError(
|
||||
"{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
|
||||
subtitleFormat.ErrorCount,
|
||||
fileExtension,
|
||||
subtitleFormat.Name);
|
||||
}
|
||||
|
||||
if (subtitle.Paragraphs.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("Unsupported format: " + fileExtension);
|
||||
_logger.LogError("{ErrorCount} errors encountered while parsing subtitle", subRip.ErrorCount);
|
||||
}
|
||||
|
||||
var trackInfo = new SubtitleTrackInfo();
|
||||
@@ -85,36 +57,5 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
trackInfo.TrackEvents = trackEvents;
|
||||
return trackInfo;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SupportsFileExtension(string fileExtension)
|
||||
=> _subtitleFormats.ContainsKey(fileExtension);
|
||||
|
||||
private IEnumerable<SubtitleFormat> GetSubtitleFormats()
|
||||
{
|
||||
var subtitleFormats = new List<SubtitleFormat>();
|
||||
var assembly = typeof(SubtitleFormat).Assembly;
|
||||
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
if (!type.IsSubclassOf(typeof(SubtitleFormat)) || type.IsAbstract)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// It shouldn't be null, but the exception is caught if it is
|
||||
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!;
|
||||
subtitleFormats.Add(subtitleFormat);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to create instance of the subtitle format {SubtitleFormatType}", type.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return subtitleFormats;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly ISubtitleParser _subtitleParser;
|
||||
|
||||
/// <summary>
|
||||
/// The _semaphoreLocks.
|
||||
@@ -49,8 +48,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
IFileSystem fileSystem,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
ISubtitleParser subtitleParser)
|
||||
IMediaSourceManager mediaSourceManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_appPaths = appPaths;
|
||||
@@ -58,7 +56,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_subtitleParser = subtitleParser;
|
||||
}
|
||||
|
||||
private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
|
||||
@@ -76,7 +73,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
try
|
||||
{
|
||||
var trackInfo = _subtitleParser.Parse(stream, inputFormat);
|
||||
var reader = GetReader(inputFormat);
|
||||
var trackInfo = reader.Parse(stream, cancellationToken);
|
||||
|
||||
FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
|
||||
|
||||
@@ -235,21 +233,54 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
|
||||
.TrimStart('.');
|
||||
|
||||
// Fallback to ffmpeg conversion
|
||||
if (!_subtitleParser.SupportsFileExtension(currentFormat))
|
||||
if (!TryGetReader(currentFormat, out _))
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
// It's possible that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs)
|
||||
// It's possbile that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs)
|
||||
return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
|
||||
}
|
||||
|
||||
private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value)
|
||||
{
|
||||
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = new SrtParser(_logger);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = new SsaParser(_logger);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = new AssParser(_logger);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private ISubtitleParser GetReader(string format)
|
||||
{
|
||||
if (TryGetReader(format, out var reader))
|
||||
{
|
||||
return reader;
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported format: " + format);
|
||||
}
|
||||
|
||||
private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
|
||||
{
|
||||
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -320,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);
|
||||
|
||||
@@ -336,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
|
||||
@@ -348,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>
|
||||
@@ -357,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));
|
||||
@@ -371,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"
|
||||
@@ -389,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);
|
||||
|
||||
@@ -540,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,
|
||||
@@ -549,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);
|
||||
|
||||
@@ -575,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)
|
||||
{
|
||||
@@ -698,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;
|
||||
|
||||
@@ -723,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);
|
||||
|
||||
@@ -23,7 +23,7 @@ public class BrandingOptions
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable the splashscreen.
|
||||
/// </summary>
|
||||
public bool SplashscreenEnabled { get; set; } = true;
|
||||
public bool SplashscreenEnabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the splashscreen location on disk.
|
||||
|
||||
@@ -21,20 +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 = 0;
|
||||
VppTonemappingContrast = 1.2;
|
||||
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;
|
||||
@@ -81,12 +82,12 @@ 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; }
|
||||
|
||||
@@ -136,12 +136,26 @@ namespace MediaBrowser.Model.Dlna
|
||||
return !condition.IsRequired;
|
||||
}
|
||||
|
||||
if (int.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var expected))
|
||||
var conditionType = condition.Condition;
|
||||
if (condition.Condition == ProfileConditionType.EqualsAny)
|
||||
{
|
||||
switch (condition.Condition)
|
||||
foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
|
||||
{
|
||||
if (int.TryParse(singleConditionString, NumberStyles.Integer, CultureInfo.InvariantCulture, out int conditionValue)
|
||||
&& conditionValue.Equals(currentValue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (int.TryParse(condition.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var expected))
|
||||
{
|
||||
switch (conditionType)
|
||||
{
|
||||
case ProfileConditionType.Equals:
|
||||
case ProfileConditionType.EqualsAny:
|
||||
return currentValue.Value.Equals(expected);
|
||||
case ProfileConditionType.GreaterThanEqual:
|
||||
return currentValue.Value >= expected;
|
||||
@@ -212,9 +226,24 @@ namespace MediaBrowser.Model.Dlna
|
||||
return !condition.IsRequired;
|
||||
}
|
||||
|
||||
if (double.TryParse(condition.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var expected))
|
||||
var conditionType = condition.Condition;
|
||||
if (condition.Condition == ProfileConditionType.EqualsAny)
|
||||
{
|
||||
switch (condition.Condition)
|
||||
foreach (var singleConditionString in condition.Value.AsSpan().Split('|'))
|
||||
{
|
||||
if (double.TryParse(singleConditionString, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out double conditionValue)
|
||||
&& conditionValue.Equals(currentValue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (double.TryParse(condition.Value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var expected))
|
||||
{
|
||||
switch (conditionType)
|
||||
{
|
||||
case ProfileConditionType.Equals:
|
||||
return currentValue.Value.Equals(expected);
|
||||
|
||||
@@ -23,6 +23,9 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly ITranscoderSupport _transcoderSupport;
|
||||
private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc" };
|
||||
private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" };
|
||||
private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" };
|
||||
|
||||
public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger)
|
||||
{
|
||||
@@ -436,9 +439,9 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
containerSupported = true;
|
||||
|
||||
videoSupported = videoStream != null && profile.SupportsVideoCodec(videoStream.Codec);
|
||||
videoSupported = videoStream == null || profile.SupportsVideoCodec(videoStream.Codec);
|
||||
|
||||
audioSupported = audioStream != null && profile.SupportsAudioCodec(audioStream.Codec);
|
||||
audioSupported = audioStream == null || profile.SupportsAudioCodec(audioStream.Codec);
|
||||
|
||||
if (videoSupported && audioSupported)
|
||||
{
|
||||
@@ -447,18 +450,17 @@ namespace MediaBrowser.Model.Dlna
|
||||
}
|
||||
}
|
||||
|
||||
var list = new List<TranscodeReason>();
|
||||
if (!containerSupported)
|
||||
{
|
||||
reasons |= TranscodeReason.ContainerNotSupported;
|
||||
}
|
||||
|
||||
if (videoStream != null && !videoSupported)
|
||||
if (!videoSupported)
|
||||
{
|
||||
reasons |= TranscodeReason.VideoCodecNotSupported;
|
||||
}
|
||||
|
||||
if (audioStream != null && !audioSupported)
|
||||
if (!audioSupported)
|
||||
{
|
||||
reasons |= TranscodeReason.AudioCodecNotSupported;
|
||||
}
|
||||
@@ -590,21 +592,19 @@ namespace MediaBrowser.Model.Dlna
|
||||
}
|
||||
|
||||
// Collect candidate audio streams
|
||||
IEnumerable<MediaStream> candidateAudioStreams = audioStream == null ? Array.Empty<MediaStream>() : new[] { audioStream };
|
||||
ICollection<MediaStream> candidateAudioStreams = audioStream == null ? Array.Empty<MediaStream>() : new[] { audioStream };
|
||||
if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0)
|
||||
{
|
||||
if (audioStream?.IsDefault == true)
|
||||
{
|
||||
candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault);
|
||||
candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language);
|
||||
candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
candidateAudioStreams = candidateAudioStreams.ToArray();
|
||||
|
||||
var videoStream = item.VideoStream;
|
||||
|
||||
var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay);
|
||||
@@ -735,7 +735,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
if (options.AllowVideoStreamCopy)
|
||||
{
|
||||
// prefer direct copy profile
|
||||
float videoFramerate = videoStream == null ? 0 : videoStream.AverageFrameRate ?? videoStream.AverageFrameRate ?? 0;
|
||||
float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0;
|
||||
TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
|
||||
int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
|
||||
int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
|
||||
@@ -746,7 +746,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream?.Codec))
|
||||
{
|
||||
var videoCodec = transcodingProfile.VideoCodec;
|
||||
var videoCodec = videoStream?.Codec;
|
||||
var container = transcodingProfile.Container;
|
||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.Video &&
|
||||
@@ -773,6 +773,13 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
// Prefer matching video codecs
|
||||
var videoCodecs = ContainerProfile.SplitValue(videoCodec);
|
||||
|
||||
// Enforce HLS video codec restrictions
|
||||
if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
videoCodecs = videoCodecs.Where(codec => _supportedHlsVideoCodecs.Contains(codec)).ToArray();
|
||||
}
|
||||
|
||||
var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null;
|
||||
if (directVideoCodec != null)
|
||||
{
|
||||
@@ -808,6 +815,20 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
// Prefer matching audio codecs, could do better here
|
||||
var audioCodecs = ContainerProfile.SplitValue(audioCodec);
|
||||
|
||||
// Enforce HLS audio codec restrictions
|
||||
if (string.Equals(playlistItem.SubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(playlistItem.Container, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsMp4.Contains(codec)).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
audioCodecs = audioCodecs.Where(codec => _supportedHlsAudioCodecsTs.Contains(codec)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec));
|
||||
playlistItem.AudioCodecs = audioCodecs;
|
||||
if (directAudioStream != null)
|
||||
@@ -853,7 +874,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.Video &&
|
||||
i.ContainsAnyCodec(videoCodec, container) &&
|
||||
i.ContainsAnyCodec(videoStream?.Codec, container) &&
|
||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
|
||||
var isFirstAppliedCodecProfile = true;
|
||||
foreach (var i in appliedVideoConditions)
|
||||
@@ -885,7 +906,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
var appliedAudioConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.VideoAudio &&
|
||||
i.ContainsAnyCodec(audioCodec, container) &&
|
||||
i.ContainsAnyCodec(audioStream?.Codec, container) &&
|
||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, audioProfile, isSecondaryAudio)));
|
||||
isFirstAppliedCodecProfile = true;
|
||||
foreach (var i in appliedAudioConditions)
|
||||
@@ -1060,7 +1081,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
MediaSourceInfo mediaSource,
|
||||
MediaStream videoStream,
|
||||
MediaStream audioStream,
|
||||
IEnumerable<MediaStream> candidateAudioStreams,
|
||||
ICollection<MediaStream> candidateAudioStreams,
|
||||
MediaStream subtitleStream,
|
||||
bool isEligibleForDirectPlay,
|
||||
bool isEligibleForDirectStream)
|
||||
@@ -1091,9 +1112,6 @@ namespace MediaBrowser.Model.Dlna
|
||||
bool? isInterlaced = videoStream?.IsInterlaced;
|
||||
string videoCodecTag = videoStream?.CodecTag;
|
||||
bool? isAvc = videoStream?.IsAVC;
|
||||
// Audio
|
||||
var defaultLanguage = audioStream?.Language ?? string.Empty;
|
||||
var defaultMarked = audioStream?.IsDefault ?? false;
|
||||
|
||||
TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : mediaSource.Timestamp;
|
||||
int? packetLength = videoStream?.PacketLength;
|
||||
@@ -1120,12 +1138,13 @@ namespace MediaBrowser.Model.Dlna
|
||||
profile,
|
||||
"VideoCodecProfile",
|
||||
profile.CodecProfiles
|
||||
.Where(codecProfile => codecProfile.Type == CodecType.Video && codecProfile.ContainsAnyCodec(videoStream?.Codec, container) &&
|
||||
.Where(codecProfile => codecProfile.Type == CodecType.Video &&
|
||||
codecProfile.ContainsAnyCodec(videoStream?.Codec, container) &&
|
||||
!checkVideoConditions(codecProfile.ApplyConditions).Any())
|
||||
.SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions)));
|
||||
|
||||
// Check audiocandidates profile conditions
|
||||
var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream, defaultLanguage, defaultMarked));
|
||||
var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream));
|
||||
|
||||
TranscodeReason subtitleProfileReasons = 0;
|
||||
if (subtitleStream != null)
|
||||
@@ -1150,7 +1169,6 @@ namespace MediaBrowser.Model.Dlna
|
||||
var reason = a & flag;
|
||||
if (reason != 0)
|
||||
{
|
||||
a = reason;
|
||||
return index;
|
||||
}
|
||||
|
||||
@@ -1160,6 +1178,8 @@ namespace MediaBrowser.Model.Dlna
|
||||
return index;
|
||||
};
|
||||
|
||||
var containerSupported = false;
|
||||
|
||||
// Check DirectPlay profiles to see if it can be direct played
|
||||
var analyzedProfiles = profile.DirectPlayProfiles
|
||||
.Where(directPlayProfile => directPlayProfile.Type == DlnaProfileType.Video)
|
||||
@@ -1173,6 +1193,10 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
directPlayProfileReasons |= TranscodeReason.ContainerNotSupported;
|
||||
}
|
||||
else
|
||||
{
|
||||
containerSupported = true;
|
||||
}
|
||||
|
||||
// Check video codec
|
||||
string videoCodec = videoStream?.Codec;
|
||||
@@ -1182,14 +1206,18 @@ namespace MediaBrowser.Model.Dlna
|
||||
}
|
||||
|
||||
// Check audio codec
|
||||
var selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
|
||||
if (selectedAudioStream == null)
|
||||
MediaStream selectedAudioStream = null;
|
||||
if (candidateAudioStreams.Any())
|
||||
{
|
||||
directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported;
|
||||
}
|
||||
else
|
||||
{
|
||||
audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream);
|
||||
selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
|
||||
if (selectedAudioStream == null)
|
||||
{
|
||||
directPlayProfileReasons |= TranscodeReason.AudioCodecNotSupported;
|
||||
}
|
||||
else
|
||||
{
|
||||
audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream);
|
||||
}
|
||||
}
|
||||
|
||||
var failureReasons = directPlayProfileReasons | containerProfileReasons | subtitleProfileReasons;
|
||||
@@ -1211,7 +1239,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
playMethod = PlayMethod.DirectPlay;
|
||||
}
|
||||
else if (directStreamFailureReasons == 0 && isEligibleForDirectStream && mediaSource.SupportsDirectStream && directPlayProfile != null)
|
||||
else if (directStreamFailureReasons == 0 && isEligibleForDirectStream && mediaSource.SupportsDirectStream)
|
||||
{
|
||||
playMethod = PlayMethod.DirectStream;
|
||||
}
|
||||
@@ -1233,7 +1261,10 @@ namespace MediaBrowser.Model.Dlna
|
||||
return profileMatch;
|
||||
}
|
||||
|
||||
var failureReasons = analyzedProfiles[false].Select(analysis => analysis.Result).FirstOrDefault().TranscodeReason;
|
||||
var failureReasons = analyzedProfiles[false]
|
||||
.Select(analysis => analysis.Result)
|
||||
.Where(result => !containerSupported || (result.TranscodeReason & TranscodeReason.ContainerNotSupported) == 0)
|
||||
.FirstOrDefault().TranscodeReason;
|
||||
if (failureReasons == 0)
|
||||
{
|
||||
failureReasons = TranscodeReason.DirectPlayError;
|
||||
@@ -1242,10 +1273,10 @@ namespace MediaBrowser.Model.Dlna
|
||||
return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons);
|
||||
}
|
||||
|
||||
private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream, string language, bool isDefault)
|
||||
private TranscodeReason CheckVideoAudioStreamDirectPlay(VideoOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream)
|
||||
{
|
||||
var profile = options.Profile;
|
||||
var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, !audioStream.IsDefault);
|
||||
var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream));
|
||||
|
||||
var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions);
|
||||
if (audioStream?.IsExternal == true)
|
||||
@@ -1529,7 +1560,8 @@ namespace MediaBrowser.Model.Dlna
|
||||
bool? isSecondaryAudio)
|
||||
{
|
||||
return codecProfiles
|
||||
.Where(profile => profile.Type == CodecType.VideoAudio && profile.ContainsAnyCodec(codec, container) &&
|
||||
.Where(profile => profile.Type == CodecType.VideoAudio &&
|
||||
profile.ContainsAnyCodec(codec, container) &&
|
||||
profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio)))
|
||||
.SelectMany(profile => profile.Conditions)
|
||||
.Where(condition => !ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio));
|
||||
@@ -1546,7 +1578,8 @@ namespace MediaBrowser.Model.Dlna
|
||||
bool checkConditions)
|
||||
{
|
||||
var conditions = codecProfiles
|
||||
.Where(profile => profile.Type == CodecType.Audio && profile.ContainsAnyCodec(codec, container) &&
|
||||
.Where(profile => profile.Type == CodecType.Audio &&
|
||||
profile.ContainsAnyCodec(codec, container) &&
|
||||
profile.ApplyConditions.All(applyCondition => ConditionProcessor.IsAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth)))
|
||||
.SelectMany(profile => profile.Conditions);
|
||||
|
||||
|
||||
@@ -230,19 +230,15 @@ namespace MediaBrowser.Model.Dto
|
||||
|
||||
public bool? IsSecondaryAudio(MediaStream stream)
|
||||
{
|
||||
// Look for the first audio track marked as default
|
||||
foreach (var currentStream in MediaStreams)
|
||||
if (stream.IsExternal)
|
||||
{
|
||||
if (currentStream.Type == MediaStreamType.Audio && currentStream.IsDefault)
|
||||
{
|
||||
return currentStream.Index != stream.Index;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look for the first audio track
|
||||
foreach (var currentStream in MediaStreams)
|
||||
{
|
||||
if (currentStream.Type == MediaStreamType.Audio)
|
||||
if (currentStream.Type == MediaStreamType.Audio && !currentStream.IsExternal)
|
||||
{
|
||||
return currentStream.Index != stream.Index;
|
||||
}
|
||||
|
||||
@@ -594,6 +594,10 @@ namespace MediaBrowser.Model.Entities
|
||||
<= 426 when Height <= 240 => IsInterlaced ? "240i" : "240p",
|
||||
// 640x360 (16:9 square pixel format)
|
||||
<= 640 when Height <= 360 => IsInterlaced ? "360i" : "360p",
|
||||
// 682x384 (16:9 square pixel format)
|
||||
<= 682 when Height <= 384 => IsInterlaced ? "384i" : "384p",
|
||||
// 720x404 (16:9 square pixel format)
|
||||
<= 720 when Height <= 404 => IsInterlaced ? "404i" : "404p",
|
||||
// 854x480 (16:9 square pixel format)
|
||||
<= 854 when Height <= 480 => IsInterlaced ? "480i" : "480p",
|
||||
// 960x544 (16:9 square pixel format)
|
||||
|
||||
@@ -23,6 +23,11 @@ namespace MediaBrowser.Model.Entities
|
||||
/// <summary>
|
||||
/// The embedded image.
|
||||
/// </summary>
|
||||
EmbeddedImage
|
||||
EmbeddedImage,
|
||||
|
||||
/// <summary>
|
||||
/// The data.
|
||||
/// </summary>
|
||||
Data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.8.2</VersionPrefix>
|
||||
<VersionPrefix>10.8.13</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
@@ -34,13 +34,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
|
||||
<PackageReference Include="MimeTypes" Version="2.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.5" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -181,6 +181,10 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
contentType = "image/png";
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new HttpRequestException("Invalid image received: contentType not set.", null, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
// thetvdb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons...
|
||||
|
||||
@@ -176,9 +176,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
var format = imageStream.Codec switch
|
||||
{
|
||||
"bmp" => ImageFormat.Bmp,
|
||||
"gif" => ImageFormat.Gif,
|
||||
"mjpeg" => ImageFormat.Jpg,
|
||||
"png" => ImageFormat.Png,
|
||||
"gif" => ImageFormat.Gif,
|
||||
"webp" => ImageFormat.Webp,
|
||||
_ => ImageFormat.Jpg
|
||||
};
|
||||
|
||||
|
||||
@@ -408,10 +408,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnglishRequested)
|
||||
{
|
||||
item.Overview = result.Plot;
|
||||
}
|
||||
item.Overview = result.Plot;
|
||||
|
||||
if (!Plugin.Instance.Configuration.CastAndCrew)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.8.2")]
|
||||
[assembly: AssemblyFileVersion("10.8.2")]
|
||||
[assembly: AssemblyVersion("10.8.13")]
|
||||
[assembly: AssemblyFileVersion("10.8.13")]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
# We just wrap `build` so this is really it
|
||||
name: "jellyfin"
|
||||
version: "10.8.2"
|
||||
version: "10.8.13"
|
||||
packages:
|
||||
- debian.amd64
|
||||
- debian.arm64
|
||||
|
||||
66
debian/changelog
vendored
66
debian/changelog
vendored
@@ -1,3 +1,69 @@
|
||||
jellyfin-server (10.8.13-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.13; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.13
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 28 Nov 2023 22:21:55 -0500
|
||||
|
||||
jellyfin-server (10.8.12-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.12; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.12
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 04 Nov 2023 14:42:37 -0400
|
||||
|
||||
jellyfin-server (10.8.11-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.11; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.11
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 23 Sep 2023 21:40:37 -0400
|
||||
|
||||
jellyfin-server (10.8.10-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.10; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.10
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 23 Apr 2023 11:02:05 -0400
|
||||
|
||||
jellyfin-server (10.8.9-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.9; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.9
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 22 Jan 2023 14:09:37 -0500
|
||||
|
||||
jellyfin-server (10.8.8-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.8; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.8
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 29 Nov 2022 13:42:47 -0500
|
||||
|
||||
jellyfin-server (10.8.7-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.7
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 31 Oct 2022 23:07:06 -0400
|
||||
|
||||
jellyfin-server (10.8.6-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.6
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 28 Oct 2022 22:41:01 -0400
|
||||
|
||||
jellyfin-server (10.8.5-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.5
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 24 Sep 2022 22:01:56 -0400
|
||||
|
||||
jellyfin-server (10.8.4-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.4
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 13 Aug 2022 21:51:45 -0400
|
||||
|
||||
jellyfin-server (10.8.3-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.3
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 01 Aug 2022 20:19:48 -0400
|
||||
|
||||
jellyfin-server (10.8.2-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version 10.8.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.2
|
||||
|
||||
3
debian/conf/jellyfin
vendored
3
debian/conf/jellyfin
vendored
@@ -27,6 +27,9 @@ JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
|
||||
# ffmpeg binary paths, overriding the system values
|
||||
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
|
||||
|
||||
# Disable glibc dynamic heap adjustment
|
||||
MALLOC_TRIM_THRESHOLD_=131072
|
||||
|
||||
# [OPTIONAL] run Jellyfin as a headless service
|
||||
#JELLYFIN_SERVICE_OPT="--service"
|
||||
|
||||
|
||||
2
debian/jellyfin.service
vendored
2
debian/jellyfin.service
vendored
@@ -8,7 +8,7 @@ EnvironmentFile = /etc/default/jellyfin
|
||||
User = jellyfin
|
||||
Group = jellyfin
|
||||
WorkingDirectory = /var/lib/jellyfin
|
||||
ExecStart = /usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} ${JELLYFIN_ADDITIONAL_OPTS}
|
||||
ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
|
||||
Restart = on-failure
|
||||
TimeoutSec = 15
|
||||
SuccessExitStatus=0 143
|
||||
|
||||
2
debian/metapackage/jellyfin
vendored
2
debian/metapackage/jellyfin
vendored
@@ -5,7 +5,7 @@ Homepage: https://jellyfin.org
|
||||
Standards-Version: 3.9.2
|
||||
|
||||
Package: jellyfin
|
||||
Version: 10.8.2
|
||||
Version: 10.8.13
|
||||
Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
Depends: jellyfin-server, jellyfin-web
|
||||
Description: Provides the Jellyfin Free Software Media System
|
||||
|
||||
10
debian/postinst
vendored
10
debian/postinst
vendored
@@ -10,6 +10,8 @@ if [[ -f $DEFAULT_FILE ]]; then
|
||||
fi
|
||||
|
||||
JELLYFIN_USER=${JELLYFIN_USER:-jellyfin}
|
||||
RENDER_GROUP=${RENDER_GROUP:-render}
|
||||
VIDEO_GROUP=${VIDEO_GROUP:-video}
|
||||
|
||||
# Data directories for program data (cache, db), configs, and logs
|
||||
PROGRAMDATA=${JELLYFIN_DATA_DIRECTORY-/var/lib/$NAME}
|
||||
@@ -28,6 +30,14 @@ case "$1" in
|
||||
adduser --system --ingroup ${JELLYFIN_USER} --shell /bin/false ${JELLYFIN_USER} --no-create-home --home ${PROGRAMDATA} \
|
||||
--gecos "Jellyfin default user" > /dev/null 2>&1
|
||||
fi
|
||||
# Add jellyfin to the render group for hwa
|
||||
if [[ ! -z "$(getent group ${RENDER_GROUP})" ]]; then
|
||||
usermod -aG ${RENDER_GROUP} ${JELLYFIN_USER} > /dev/null 2>&1
|
||||
fi
|
||||
# Add jellyfin to the video group for hwa
|
||||
if [[ ! -z "$(getent group ${VIDEO_GROUP})" ]]; then
|
||||
usermod -aG ${VIDEO_GROUP} ${JELLYFIN_USER} > /dev/null 2>&1
|
||||
fi
|
||||
# ensure $PROGRAMDATA exists
|
||||
if [[ ! -d $PROGRAMDATA ]]; then
|
||||
mkdir $PROGRAMDATA
|
||||
|
||||
2
debian/rules
vendored
2
debian/rules
vendored
@@ -40,7 +40,7 @@ override_dh_clistrip:
|
||||
|
||||
override_dh_auto_build:
|
||||
dotnet publish -maxcpucount:1 --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
|
||||
"-p:DebugSymbols=false;DebugType=none" Jellyfin.Server
|
||||
-p:DebugSymbols=false -p:DebugType=none Jellyfin.Server
|
||||
|
||||
override_dh_auto_clean:
|
||||
dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true
|
||||
|
||||
@@ -13,7 +13,7 @@ RUN yum update -yq \
|
||||
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
|
||||
|
||||
# Install DotNET SDK
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -10,4 +10,4 @@ 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="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
|
||||
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
@@ -10,4 +10,4 @@ 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="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
|
||||
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
@@ -10,4 +10,4 @@ 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="${ARTIFACT_DIR}" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
|
||||
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
|
||||
|
||||
@@ -12,7 +12,7 @@ RUN dnf update -yq \
|
||||
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
|
||||
|
||||
# Install DotNET SDK
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
|
||||
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
|
||||
|
||||
# Install dotnet repository
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
|
||||
mmv build-essential lsb-release
|
||||
|
||||
# Install dotnet repository
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
|
||||
mmv build-essential lsb-release
|
||||
|
||||
# Install dotnet repository
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/0e83f50a-0619-45e6-8f16-dc4f41d1bb16/e0de908b2f070ef9e7e3b6ddea9d268c/dotnet-sdk-6.0.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ else
|
||||
fi
|
||||
|
||||
# Build archives
|
||||
dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
|
||||
dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
|
||||
tar -czf jellyfin-server_${version}_linux-amd64.tar.gz -C dist jellyfin-server_${version}
|
||||
rm -rf dist/jellyfin-server_${version}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user