mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-05-18 14:18:11 +02:00
Compare commits
47 commits
6695baf8ec
...
8497e8d695
Author | SHA1 | Date | |
---|---|---|---|
8497e8d695 | |||
b258e85f69 | |||
eb22c222a0 | |||
f453746e2c | |||
169698619d | |||
34a8cc203a | |||
bd3fdcd1d4 | |||
45d9481f73 | |||
4a4540bb0f | |||
f20a9c9b2b | |||
1accfd79da | |||
1b4f5451a5 | |||
41fd7f168b | |||
6633c26f46 | |||
16b2483d84 | |||
a1f5e7265f | |||
3feb3f81bf | |||
5dc6bb4910 | |||
48bb16472f | |||
74f3e54807 | |||
9ffb07d67f | |||
8c9d0df7f2 | |||
6f78ac2ff3 | |||
0b15352771 | |||
276ae3b8b7 | |||
2cbef3aa38 | |||
2459b7e62e | |||
2ad872001d | |||
8eff5d1bf7 | |||
88a38a61b5 | |||
935c2c97fe | |||
f63148441d | |||
5612cb8178 | |||
ccd06bc547 | |||
d29b85a134 | |||
9a515149ef | |||
ac108690a8 | |||
428283f787 | |||
3c159822b5 | |||
f5f0d18934 | |||
3936fc9f25 | |||
c791331952 | |||
f26eb15be0 | |||
bafb7f84ad | |||
374b6ca0e2 | |||
09b0229670 | |||
ab55dcf82d |
8
.github/workflows/ci-codeql-analysis.yml
vendored
8
.github/workflows/ci-codeql-analysis.yml
vendored
|
@ -20,18 +20,18 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
|
||||
uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
|
||||
uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c7f9125735019aa87cfc361530512d50ea439c71 # v3.25.1
|
||||
uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
|
|
14
.github/workflows/ci-openapi.yml
vendored
14
.github/workflows/ci-openapi.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
@ -25,7 +25,7 @@ jobs:
|
|||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # v4.3.2
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
|
@ -39,7 +39,7 @@ jobs:
|
|||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
@ -59,7 +59,7 @@ jobs:
|
|||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@1746f4ab65b179e0ea60a494b83293b640dd5bba # v4.3.2
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
|
@ -78,12 +78,12 @@ jobs:
|
|||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # v4.1.5
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # v4.1.5
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
|
@ -152,7 +152,7 @@ jobs:
|
|||
run: |-
|
||||
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@8caf195ad4b1dee92908e23f56eeb0696f1dd42d # v4.1.5
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
|
|
4
.github/workflows/ci-tests.yml
vendored
4
.github/workflows/ci-tests.yml
vendored
|
@ -19,7 +19,7 @@ jobs:
|
|||
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
|
||||
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||
with:
|
||||
|
@ -34,7 +34,7 @@ jobs:
|
|||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@3e39bd1b454c2bac14560547e4394f9317672705 # 5.2.4
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@2a2d60ea1c7e811f54684179af6ac1ae8c1ce69a # 5.2.5
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
|
6
.github/workflows/commands.yml
vendored
6
.github/workflows/commands.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
|||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
@ -51,7 +51,7 @@ jobs:
|
|||
reactions: eyes
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
@ -128,7 +128,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
|
|
2
.github/workflows/issue-template-check.yml
vendored
2
.github/workflows/issue-template-check.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
issues: write
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
|
|
4
.github/workflows/release-bump-version.yaml
vendored
4
.github/workflows/release-bump-version.yaml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
|||
yq-version: v4.9.8
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
|
@ -66,7 +66,7 @@ jobs:
|
|||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
<PackageVersion Include="BlurHashSharp" Version="1.3.2" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.27" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.4.2" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.4.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
|
||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||
|
|
|
@ -127,15 +127,11 @@ namespace Emby.Server.Implementations.AppBase
|
|||
|
||||
if (_configurationFactories is null)
|
||||
{
|
||||
_configurationFactories = new[] { factory };
|
||||
_configurationFactories = [factory];
|
||||
}
|
||||
else
|
||||
{
|
||||
var oldLen = _configurationFactories.Length;
|
||||
var arr = new IConfigurationFactory[oldLen + 1];
|
||||
_configurationFactories.CopyTo(arr, 0);
|
||||
arr[oldLen] = factory;
|
||||
_configurationFactories = arr;
|
||||
_configurationFactories = [.._configurationFactories, factory];
|
||||
}
|
||||
|
||||
_configurationStores = _configurationFactories
|
||||
|
|
|
@ -49,8 +49,8 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
private const string SaveItemCommandText =
|
||||
@"replace into TypedBaseItems
|
||||
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
|
||||
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
|
||||
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,NormalizationGain,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
|
||||
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@NormalizationGain,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
@ -111,6 +111,7 @@ namespace Emby.Server.Implementations.Data
|
|||
"DateLastMediaAdded",
|
||||
"Album",
|
||||
"LUFS",
|
||||
"NormalizationGain",
|
||||
"CriticRating",
|
||||
"IsVirtualItem",
|
||||
"SeriesName",
|
||||
|
@ -478,6 +479,7 @@ namespace Emby.Server.Implementations.Data
|
|||
AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
|
||||
AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
|
||||
AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
|
||||
AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames);
|
||||
AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
|
||||
AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
|
||||
AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
|
||||
|
@ -886,6 +888,7 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
saveItemStatement.TryBind("@Album", item.Album);
|
||||
saveItemStatement.TryBind("@LUFS", item.LUFS);
|
||||
saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain);
|
||||
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
|
||||
|
||||
if (item is IHasSeries hasSeriesName)
|
||||
|
@ -1672,6 +1675,11 @@ namespace Emby.Server.Implementations.Data
|
|||
item.LUFS = lUFS;
|
||||
}
|
||||
|
||||
if (reader.TryGetSingle(index++, out var normalizationGain))
|
||||
{
|
||||
item.NormalizationGain = normalizationGain;
|
||||
}
|
||||
|
||||
if (reader.TryGetSingle(index++, out var criticRating))
|
||||
{
|
||||
item.CriticRating = criticRating;
|
||||
|
@ -2315,14 +2323,7 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
columns.Add(builder.ToString());
|
||||
|
||||
var oldLen = query.ExcludeItemIds.Length;
|
||||
var newLen = oldLen + item.ExtraIds.Length + 1;
|
||||
var excludeIds = new Guid[newLen];
|
||||
query.ExcludeItemIds.CopyTo(excludeIds, 0);
|
||||
excludeIds[oldLen] = item.Id;
|
||||
item.ExtraIds.CopyTo(excludeIds, oldLen + 1);
|
||||
|
||||
query.ExcludeItemIds = excludeIds;
|
||||
query.ExcludeItemIds = [..query.ExcludeItemIds, item.Id, ..item.ExtraIds];
|
||||
query.ExcludeProviderIds = item.ProviderIds;
|
||||
}
|
||||
|
||||
|
@ -2830,10 +2831,7 @@ namespace Emby.Server.Implementations.Data
|
|||
prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
|
||||
}
|
||||
|
||||
var arr = new (ItemSortBy, SortOrder)[prepend.Count + orderBy.Count];
|
||||
prepend.CopyTo(arr, 0);
|
||||
orderBy.CopyTo(arr, prepend.Count);
|
||||
orderBy = query.OrderBy = arr;
|
||||
orderBy = query.OrderBy = [..prepend, ..orderBy];
|
||||
}
|
||||
else if (orderBy.Count == 0)
|
||||
{
|
||||
|
@ -4194,7 +4192,19 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
int index = 0;
|
||||
string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
|
||||
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
|
||||
// Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client.
|
||||
// In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well.
|
||||
if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
|
||||
{
|
||||
whereClauses.Add($"""
|
||||
((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null
|
||||
OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null)
|
||||
""");
|
||||
}
|
||||
else
|
||||
{
|
||||
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -898,7 +898,15 @@ namespace Emby.Server.Implementations.Dto
|
|||
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
|
||||
}
|
||||
|
||||
dto.LUFS = item.LUFS;
|
||||
if (item.LUFS.HasValue)
|
||||
{
|
||||
// -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
|
||||
dto.NormalizationGain = -18f - item.LUFS;
|
||||
}
|
||||
else if (item.NormalizationGain.HasValue)
|
||||
{
|
||||
dto.NormalizationGain = item.NormalizationGain;
|
||||
}
|
||||
|
||||
// Add audio info
|
||||
if (item is Audio audio)
|
||||
|
|
|
@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
};
|
||||
|
||||
private static readonly Glob[] _globs = _patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
|
||||
private static readonly Glob[] _globs = Array.ConvertAll(_patterns, p => Glob.Parse(p, _globOptions));
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the supplied path should be ignored.
|
||||
|
|
|
@ -44,7 +44,6 @@ using MediaBrowser.Model.Library;
|
|||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TMDbLib.Objects.Authentication;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
|
||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
||||
|
@ -1612,14 +1611,18 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <returns>IEnumerable{System.String}.</returns>
|
||||
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
|
||||
{
|
||||
if (IntroProviders.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var tasks = IntroProviders
|
||||
.Take(1)
|
||||
.Select(i => GetIntros(i, item, user));
|
||||
|
||||
var items = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
return items
|
||||
.SelectMany(i => i.ToArray())
|
||||
.SelectMany(i => i)
|
||||
.Select(ResolveIntro)
|
||||
.Where(i => i is not null)!; // null values got filtered out
|
||||
}
|
||||
|
@ -3035,9 +3038,7 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
|
||||
|
||||
var list = libraryOptions.PathInfos.ToList();
|
||||
list.Add(pathInfo);
|
||||
libraryOptions.PathInfos = list.ToArray();
|
||||
libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo];
|
||||
|
||||
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
|
||||
|
||||
|
@ -3056,8 +3057,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
|
||||
|
||||
var list = libraryOptions.PathInfos.ToList();
|
||||
foreach (var originalPathInfo in list)
|
||||
foreach (var originalPathInfo in libraryOptions.PathInfos)
|
||||
{
|
||||
if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal))
|
||||
{
|
||||
|
@ -3066,8 +3066,6 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
|
||||
libraryOptions.PathInfos = list.ToArray();
|
||||
|
||||
CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
|
||||
}
|
||||
|
||||
|
|
|
@ -274,7 +274,7 @@ namespace Emby.Server.Implementations.Library
|
|||
var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken));
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
return results.SelectMany(i => i.ToList());
|
||||
return results.SelectMany(i => i);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, IMediaSourceProvider provider, CancellationToken cancellationToken)
|
||||
|
|
|
@ -13,7 +13,6 @@ using MediaBrowser.Controller.Entities.Audio;
|
|||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
|
@ -303,8 +303,8 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
// Handle situations with the grouping setting, e.g. movies showing up in tv, etc.
|
||||
// Thanks to mixed content libraries included in the UserView
|
||||
var hasCollectionType = parents.OfType<UserView>().ToArray();
|
||||
if (hasCollectionType.Length > 0)
|
||||
var hasCollectionType = parents.OfType<UserView>().ToList();
|
||||
if (hasCollectionType.Count > 0)
|
||||
{
|
||||
if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
|
||||
{
|
||||
|
|
|
@ -126,5 +126,7 @@
|
|||
"External": "خارجي",
|
||||
"HearingImpaired": "ضعاف السمع",
|
||||
"TaskRefreshTrickplayImages": "توليد صور Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة."
|
||||
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
|
||||
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة."
|
||||
}
|
||||
|
|
|
@ -128,5 +128,7 @@
|
|||
"TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció"
|
||||
"TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció",
|
||||
"TaskAudioNormalization": "Normalització d'Àudio",
|
||||
"TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio."
|
||||
}
|
||||
|
|
|
@ -128,5 +128,7 @@
|
|||
"TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Pročistit kolekce a seznamy přehrávání",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Odstraní neexistující položky z kolekcí a seznamů přehrávání.",
|
||||
"TaskAudioNormalization": "Normalizace zvuku",
|
||||
"TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku."
|
||||
}
|
||||
|
|
|
@ -128,5 +128,7 @@
|
|||
"TaskRefreshTrickplayImages": "Generate Trickplay Images",
|
||||
"TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
|
||||
"TaskAudioNormalization": "Audio Normalisation",
|
||||
"TaskAudioNormalizationDescription": "Scans files for audio normalisation data."
|
||||
}
|
||||
|
|
|
@ -106,6 +106,8 @@
|
|||
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
|
||||
"TaskRefreshChapterImages": "Extract Chapter Images",
|
||||
"TaskRefreshChapterImagesDescription": "Creates thumbnails for videos that have chapters.",
|
||||
"TaskAudioNormalization": "Audio Normalization",
|
||||
"TaskAudioNormalizationDescription": "Scans files for audio normalization data.",
|
||||
"TaskRefreshLibrary": "Scan Media Library",
|
||||
"TaskRefreshLibraryDescription": "Scans your media library for new files and refreshes metadata.",
|
||||
"TaskCleanLogs": "Clean Log Directory",
|
||||
|
|
|
@ -126,5 +126,7 @@
|
|||
"External": "Externe",
|
||||
"HearingImpaired": "Malentendants",
|
||||
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
|
||||
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Supprimer les liens inexistants des collections et des listes de lecture"
|
||||
}
|
||||
|
|
|
@ -128,5 +128,7 @@
|
|||
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
|
||||
"TaskAudioNormalization": "Normalisation audio",
|
||||
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio."
|
||||
}
|
||||
|
|
|
@ -124,5 +124,6 @@
|
|||
"TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.",
|
||||
"TaskKeyframeExtractor": "키프레임 추출",
|
||||
"External": "외부",
|
||||
"HearingImpaired": "청각 장애"
|
||||
"HearingImpaired": "청각 장애",
|
||||
"TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리"
|
||||
}
|
||||
|
|
|
@ -123,5 +123,7 @@
|
|||
"HearingImpaired": "കേൾവി തകരാറുകൾ",
|
||||
"External": "പുറമേയുള്ള",
|
||||
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്സ്ട്രാക്റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
|
||||
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
|
||||
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "നിലവിലില്ലാത്ത ശേഖരങ്ങളിൽ നിന്നും പ്ലേലിസ്റ്റുകളിൽ നിന്നും ഇനങ്ങൾ നീക്കംചെയ്യുന്നു.",
|
||||
"TaskCleanCollectionsAndPlaylists": "ശേഖരങ്ങളും പ്ലേലിസ്റ്റുകളും വൃത്തിയാക്കുക"
|
||||
}
|
||||
|
|
|
@ -128,5 +128,7 @@
|
|||
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
|
||||
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Verwijdert niet langer bestaande items uit collecties en afspeellijsten."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Verwijdert niet langer bestaande items uit collecties en afspeellijsten.",
|
||||
"TaskAudioNormalization": "Geluidsnormalisatie",
|
||||
"TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie."
|
||||
}
|
||||
|
|
|
@ -128,5 +128,7 @@
|
|||
"TaskRefreshTrickplayImages": "Generuj obrazy trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania"
|
||||
"TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania",
|
||||
"TaskAudioNormalization": "Normalizacja dźwięku",
|
||||
"TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku."
|
||||
}
|
||||
|
|
|
@ -126,5 +126,7 @@
|
|||
"External": "Externo",
|
||||
"HearingImpaired": "Deficiência Auditiva",
|
||||
"TaskRefreshTrickplayImages": "Gerar imagens Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Cria prévias Trickplay para vídeos em bibliotecas em que o recurso está habilitado."
|
||||
"TaskRefreshTrickplayImagesDescription": "Cria prévias Trickplay para vídeos em bibliotecas em que o recurso está habilitado.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais."
|
||||
}
|
||||
|
|
|
@ -128,5 +128,7 @@
|
|||
"TaskRefreshTrickplayImages": "Gerar imagens de truques",
|
||||
"TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução"
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
|
||||
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
|
||||
"TaskAudioNormalization": "Normalização de áudio"
|
||||
}
|
||||
|
|
|
@ -127,5 +127,7 @@
|
|||
"TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
|
||||
"TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução"
|
||||
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
|
||||
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",
|
||||
"TaskAudioNormalization": "Normalização de áudio"
|
||||
}
|
||||
|
|
|
@ -128,5 +128,7 @@
|
|||
"TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Vyčistiť kolekcie a playlisty",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú.",
|
||||
"TaskAudioNormalization": "Normalizácia zvuku",
|
||||
"TaskAudioNormalizationDescription": "Skenovať súbory za účelom normalizácie zvuku."
|
||||
}
|
||||
|
|
|
@ -127,5 +127,7 @@
|
|||
"TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
|
||||
"TaskRefreshTrickplayImages": "Створити Trickplay-зображення",
|
||||
"TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують.",
|
||||
"TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.",
|
||||
"TaskAudioNormalization": "Нормалізація аудіо"
|
||||
}
|
||||
|
|
|
@ -226,13 +226,8 @@ namespace Emby.Server.Implementations.Playlists
|
|||
return;
|
||||
}
|
||||
|
||||
// Create a new array with the updated playlist items
|
||||
var newLinkedChildren = new LinkedChild[playlist.LinkedChildren.Length + childrenToAdd.Count];
|
||||
playlist.LinkedChildren.CopyTo(newLinkedChildren, 0);
|
||||
childrenToAdd.CopyTo(newLinkedChildren, playlist.LinkedChildren.Length);
|
||||
|
||||
// Update the playlist in the repository
|
||||
playlist.LinkedChildren = newLinkedChildren;
|
||||
playlist.LinkedChildren = [..playlist.LinkedChildren, ..childrenToAdd];
|
||||
|
||||
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
|
||||
|
||||
|
@ -526,8 +521,8 @@ namespace Emby.Server.Implementations.Playlists
|
|||
foreach (var playlist in playlists)
|
||||
{
|
||||
// Update owner if shared
|
||||
var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToArray();
|
||||
if (rankedShares.Length > 0)
|
||||
var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToList();
|
||||
if (rankedShares.Count > 0)
|
||||
{
|
||||
playlist.OwnerUserId = rankedShares[0].UserId;
|
||||
playlist.Shares = rankedShares.Skip(1).ToArray();
|
||||
|
|
|
@ -256,8 +256,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
{
|
||||
get
|
||||
{
|
||||
var triggers = InternalTriggers;
|
||||
return triggers.Select(i => i.Item1).ToArray();
|
||||
return Array.ConvertAll(InternalTriggers, i => i.Item1);
|
||||
}
|
||||
|
||||
set
|
||||
|
@ -269,7 +268,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
|
||||
SaveTriggers(triggerList);
|
||||
|
||||
InternalTriggers = triggerList.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
|
||||
InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -503,7 +502,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
private Tuple<TaskTriggerInfo, ITaskTrigger>[] LoadTriggers()
|
||||
{
|
||||
// This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
|
||||
var settings = LoadTriggerSettings().Where(i => i is not null).ToArray();
|
||||
var settings = LoadTriggerSettings().Where(i => i is not null);
|
||||
|
||||
return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// The audio normalization task.
|
||||
/// </summary>
|
||||
public partial class AudioNormalizationTask : IScheduledTask
|
||||
{
|
||||
private readonly IItemRepository _itemRepository;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly ILogger<AudioNormalizationTask> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioNormalizationTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{AudioNormalizationTask}"/> interface.</param>
|
||||
public AudioNormalizationTask(
|
||||
IItemRepository itemRepository,
|
||||
ILibraryManager libraryManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IConfigurationManager configurationManager,
|
||||
ILocalizationManager localizationManager,
|
||||
ILogger<AudioNormalizationTask> logger)
|
||||
{
|
||||
_itemRepository = itemRepository;
|
||||
_libraryManager = libraryManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_configurationManager = configurationManager;
|
||||
_localization = localizationManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskAudioNormalization");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("TaskAudioNormalizationDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "AudioNormalization";
|
||||
|
||||
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
|
||||
private static partial Regex LUFSRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var library in _libraryManager.RootFolder.Children)
|
||||
{
|
||||
var libraryOptions = _libraryManager.GetLibraryOptions(library);
|
||||
if (!libraryOptions.EnableLUFSScan)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Album gain
|
||||
var albums = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = [BaseItemKind.MusicAlbum],
|
||||
Parent = library,
|
||||
Recursive = true
|
||||
});
|
||||
|
||||
foreach (var a in albums)
|
||||
{
|
||||
if (a.NormalizationGain.HasValue || a.LUFS.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip albums that don't have multiple tracks, album gain is useless here
|
||||
var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
|
||||
if (albumTracks.Count <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), Guid.NewGuid() + ".concat");
|
||||
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
|
||||
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
|
||||
a.LUFS = await CalculateLUFSAsync(
|
||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(albums, cancellationToken);
|
||||
|
||||
// Track gain
|
||||
var tracks = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
MediaTypes = [MediaType.Audio],
|
||||
IncludeItemTypes = [BaseItemKind.Audio],
|
||||
Parent = library,
|
||||
Recursive = true
|
||||
});
|
||||
|
||||
foreach (var t in tracks)
|
||||
{
|
||||
if (t.NormalizationGain.HasValue || t.LUFS.HasValue || !t.IsFileProtocol)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken);
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(tracks, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
return
|
||||
[
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfo.TriggerInterval,
|
||||
IntervalTicks = TimeSpan.FromHours(24).Ticks
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private async Task<float?> CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken)
|
||||
{
|
||||
var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
|
||||
|
||||
using (var process = new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = args,
|
||||
RedirectStandardOutput = false,
|
||||
RedirectStandardError = true
|
||||
},
|
||||
})
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Starting ffmpeg with arguments: {Arguments}", args);
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting ffmpeg with arguments: {Arguments}", args);
|
||||
return null;
|
||||
}
|
||||
|
||||
using var reader = process.StandardError;
|
||||
var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
MatchCollection split = LUFSRegex().Matches(output);
|
||||
|
||||
if (split.Count != 0)
|
||||
{
|
||||
return float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
|
||||
_logger.LogError("Failed to find LUFS value in output:\n{Output}", output);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library;
|
|||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
|
|
|
@ -116,7 +116,7 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
|
|||
foreach (var linkedChild in folder.LinkedChildren)
|
||||
{
|
||||
var path = linkedChild.Path;
|
||||
if (Path.HasExtension(path) ? !File.Exists(path) : !Directory.Exists(path))
|
||||
if (!File.Exists(path) && !Directory.Exists(path))
|
||||
{
|
||||
_logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path);
|
||||
(itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild);
|
||||
|
|
|
@ -400,7 +400,7 @@ namespace Emby.Server.Implementations.Session
|
|||
{
|
||||
session.NowPlayingQueue = nowPlayingQueue;
|
||||
|
||||
var itemIds = nowPlayingQueue.Select(queue => queue.Id).ToArray();
|
||||
var itemIds = Array.ConvertAll(nowPlayingQueue, queue => queue.Id);
|
||||
session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos(
|
||||
_libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }),
|
||||
new DtoOptions(true));
|
||||
|
@ -1386,16 +1386,13 @@ namespace Emby.Server.Implementations.Session
|
|||
if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
|
||||
var list = session.AdditionalUsers.ToList();
|
||||
|
||||
list.Add(new SessionUserInfo
|
||||
var newUser = new SessionUserInfo
|
||||
{
|
||||
UserId = userId,
|
||||
UserName = user.Username
|
||||
});
|
||||
};
|
||||
|
||||
session.AdditionalUsers = list.ToArray();
|
||||
session.AdditionalUsers = [..session.AdditionalUsers, newUser];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||
using System.Net.Mime;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Models;
|
||||
using MediaBrowser.Common.Api;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
|
@ -45,9 +46,9 @@ public class DashboardController : BaseJellyfinApiController
|
|||
/// <response code="404">Server still loading.</response>
|
||||
/// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
|
||||
[HttpGet("web/ConfigurationPages")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Authorize]
|
||||
public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
|
||||
[FromQuery] bool? enableInMainMenu)
|
||||
{
|
||||
|
|
|
@ -1481,7 +1481,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
|
||||
if (currentTranscodingIndex.HasValue)
|
||||
{
|
||||
DeleteLastFile(playlistPath, segmentExtension, 0);
|
||||
await DeleteLastFile(playlistPath, segmentExtension, 0).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
|
||||
|
@ -1712,12 +1712,11 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
var useDownMixAlgorithm = state.AudioStream.Channels is 6 && _encodingOptions.DownMixStereoAlgorithm != DownMixStereoAlgorithms.None;
|
||||
|
||||
if (channels.HasValue
|
||||
&& (channels.Value != 2
|
||||
|| (state.AudioStream is not null
|
||||
&& state.AudioStream.Channels.HasValue
|
||||
&& state.AudioStream.Channels.Value > 5
|
||||
&& _encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None)))
|
||||
|| (state.AudioStream?.Channels != null && !useDownMixAlgorithm)))
|
||||
{
|
||||
args += " -ac " + channels.Value;
|
||||
}
|
||||
|
@ -2010,17 +2009,19 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
}
|
||||
}
|
||||
|
||||
private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
|
||||
private Task DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
|
||||
{
|
||||
var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
|
||||
|
||||
if (file is not null)
|
||||
if (file is null)
|
||||
{
|
||||
DeleteFile(file.FullName, retryCount);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return DeleteFile(file.FullName, retryCount);
|
||||
}
|
||||
|
||||
private void DeleteFile(string path, int retryCount)
|
||||
private async Task DeleteFile(string path, int retryCount)
|
||||
{
|
||||
if (retryCount >= 5)
|
||||
{
|
||||
|
@ -2037,9 +2038,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
{
|
||||
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
|
||||
|
||||
var task = Task.Delay(100);
|
||||
task.Wait();
|
||||
DeleteFile(path, retryCount + 1);
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
await DeleteFile(path, retryCount + 1).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -264,7 +264,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
|||
|
||||
if (request.Studios is not null)
|
||||
{
|
||||
item.Studios = request.Studios.Select(x => x.Name).ToArray();
|
||||
item.Studios = Array.ConvertAll(request.Studios, x => x.Name);
|
||||
}
|
||||
|
||||
if (request.DateCreated.HasValue)
|
||||
|
@ -379,10 +379,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
|||
{
|
||||
if (item is IHasAlbumArtist hasAlbumArtists)
|
||||
{
|
||||
hasAlbumArtists.AlbumArtists = request
|
||||
.AlbumArtists
|
||||
.Select(i => i.Name)
|
||||
.ToArray();
|
||||
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -390,10 +387,7 @@ public class ItemUpdateController : BaseJellyfinApiController
|
|||
{
|
||||
if (item is IHasArtist hasArtists)
|
||||
{
|
||||
hasArtists.Artists = request
|
||||
.ArtistItems
|
||||
.Select(i => i.Name)
|
||||
.ToArray();
|
||||
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -246,9 +246,9 @@ public class ItemsController : BaseJellyfinApiController
|
|||
var isApiKey = User.GetIsApiKey();
|
||||
// if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var user = !isApiKey && !userId.IsNullOrEmpty()
|
||||
? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
|
||||
: null;
|
||||
var user = userId.IsNullOrEmpty()
|
||||
? null
|
||||
: _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException();
|
||||
|
||||
// beyond this point, we're either using an api key or we have a valid user
|
||||
if (!isApiKey && user is null)
|
||||
|
@ -256,6 +256,13 @@ public class ItemsController : BaseJellyfinApiController
|
|||
return BadRequest("userId is required");
|
||||
}
|
||||
|
||||
if (user is not null
|
||||
&& user.GetPreference(PreferenceKind.AllowedTags).Length != 0
|
||||
&& !fields.Contains(ItemFields.Tags))
|
||||
{
|
||||
fields = [..fields, ItemFields.Tags];
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(User)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
|
|
@ -158,13 +158,13 @@ public class LibraryController : BaseJellyfinApiController
|
|||
return NotFound();
|
||||
}
|
||||
|
||||
IEnumerable<BaseItem> themeItems;
|
||||
IReadOnlyList<BaseItem> themeItems;
|
||||
|
||||
while (true)
|
||||
{
|
||||
themeItems = item.GetThemeSongs();
|
||||
|
||||
if (themeItems.Any() || !inheritFromParent)
|
||||
if (themeItems.Count > 0 || !inheritFromParent)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ public class LibraryStructureController : BaseJellyfinApiController
|
|||
|
||||
if (paths is not null && paths.Length > 0)
|
||||
{
|
||||
libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo(i)).ToArray();
|
||||
libraryOptions.PathInfos = Array.ConvertAll(paths, i => new MediaPathInfo(i));
|
||||
}
|
||||
|
||||
await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace Jellyfin.Api.Controllers;
|
|||
/// Package Controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
[Authorize]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public class PackageController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IInstallationManager _installationManager;
|
||||
|
@ -90,7 +90,6 @@ public class PackageController : BaseJellyfinApiController
|
|||
[HttpPost("Packages/Installed/{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public async Task<ActionResult> InstallPackage(
|
||||
[FromRoute, Required] string name,
|
||||
[FromQuery] Guid? assemblyGuid,
|
||||
|
@ -128,7 +127,6 @@ public class PackageController : BaseJellyfinApiController
|
|||
/// <response code="204">Installation cancelled.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
|
||||
[HttpDelete("Packages/Installing/{packageId}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult CancelPackageInstallation(
|
||||
[FromRoute, Required] Guid packageId)
|
||||
|
@ -156,7 +154,6 @@ public class PackageController : BaseJellyfinApiController
|
|||
/// <response code="204">Package repositories saved.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Repositories")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos)
|
||||
{
|
||||
|
|
|
@ -22,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
|
|||
/// <summary>
|
||||
/// Plugins controller.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public class PluginsController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IInstallationManager _installationManager;
|
||||
|
@ -66,7 +66,6 @@ public class PluginsController : BaseJellyfinApiController
|
|||
/// <response code="404">Plugin not found.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
|
||||
[HttpPost("{pluginId}/{version}/Enable")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
|
||||
|
@ -90,7 +89,6 @@ public class PluginsController : BaseJellyfinApiController
|
|||
/// <response code="404">Plugin not found.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
|
||||
[HttpPost("{pluginId}/{version}/Disable")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
|
||||
|
@ -114,7 +112,6 @@ public class PluginsController : BaseJellyfinApiController
|
|||
/// <response code="404">Plugin not found.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
|
||||
[HttpDelete("{pluginId}/{version}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
|
||||
|
@ -137,7 +134,6 @@ public class PluginsController : BaseJellyfinApiController
|
|||
/// <response code="404">Plugin not found.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
|
||||
[HttpDelete("{pluginId}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use the UninstallPluginByVersion API.")]
|
||||
|
|
|
@ -17,6 +17,7 @@ using MediaBrowser.Controller.MediaEncoding;
|
|||
using MediaBrowser.Controller.Streaming;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -137,6 +138,8 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||
// set device specific data
|
||||
foreach (var sourceInfo in info.MediaSources)
|
||||
{
|
||||
sourceInfo.TranscodingContainer = transcodingContainer;
|
||||
sourceInfo.TranscodingSubProtocol = transcodingProtocol ?? sourceInfo.TranscodingSubProtocol;
|
||||
_mediaInfoHelper.SetDeviceSpecificData(
|
||||
item,
|
||||
sourceInfo,
|
||||
|
@ -171,6 +174,8 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||
return Redirect(mediaSource.Path);
|
||||
}
|
||||
|
||||
// This one is currently very misleading as the SupportsDirectStream actually means "can direct play"
|
||||
// The definition of DirectStream also seems changed during development
|
||||
var isStatic = mediaSource.SupportsDirectStream;
|
||||
if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
|
@ -178,20 +183,21 @@ public class UniversalAudioController : BaseJellyfinApiController
|
|||
// ffmpeg option -> file extension
|
||||
// mpegts -> ts
|
||||
// fmp4 -> mp4
|
||||
// TODO: remove this when we switch back to the segment muxer
|
||||
var supportedHlsContainers = new[] { "ts", "mp4" };
|
||||
|
||||
// fallback to mpegts if device reports some weird value unsupported by hls
|
||||
var requestedSegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts";
|
||||
var segmentContainer = Array.Exists(supportedHlsContainers, element => element == mediaSource.TranscodingContainer) ? mediaSource.TranscodingContainer : requestedSegmentContainer;
|
||||
var dynamicHlsRequestDto = new HlsAudioRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
Container = ".m3u8",
|
||||
Static = isStatic,
|
||||
PlaySessionId = info.PlaySessionId,
|
||||
// fallback to mpegts if device reports some weird value unsupported by hls
|
||||
SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts",
|
||||
SegmentContainer = segmentContainer,
|
||||
MediaSourceId = mediaSourceId,
|
||||
DeviceId = deviceId,
|
||||
AudioCodec = audioCodec,
|
||||
AudioCodec = mediaSource.TranscodeReasons == TranscodeReason.ContainerNotSupported ? "copy" : audioCodec,
|
||||
EnableAutoStreamCopy = true,
|
||||
AllowAudioStreamCopy = true,
|
||||
AllowVideoStreamCopy = true,
|
||||
|
|
|
@ -85,16 +85,11 @@ public class UserViewsController : BaseJellyfinApiController
|
|||
var folders = _userViewManager.GetUserViews(query);
|
||||
|
||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||
var fields = dtoOptions.Fields.ToList();
|
||||
|
||||
fields.Add(ItemFields.PrimaryImageAspectRatio);
|
||||
fields.Add(ItemFields.DisplayPreferencesId);
|
||||
dtoOptions.Fields = fields.ToArray();
|
||||
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId];
|
||||
|
||||
var user = _userManager.GetUserById(userId.Value);
|
||||
|
||||
var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
|
||||
.ToArray();
|
||||
var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user));
|
||||
|
||||
return new QueryResult<BaseItemDto>(dtos);
|
||||
}
|
||||
|
|
|
@ -43,11 +43,7 @@ public static class DtoExtensions
|
|||
client.Contains("media center", StringComparison.OrdinalIgnoreCase) ||
|
||||
client.Contains("classic", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
int oldLen = dtoOptions.Fields.Count;
|
||||
var arr = new ItemFields[oldLen + 1];
|
||||
dtoOptions.Fields.CopyTo(arr, 0);
|
||||
arr[oldLen] = ItemFields.RecursiveItemCount;
|
||||
dtoOptions.Fields = arr;
|
||||
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.RecursiveItemCount];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,11 +57,7 @@ public static class DtoExtensions
|
|||
client.Contains("samsung", StringComparison.OrdinalIgnoreCase) ||
|
||||
client.Contains("androidtv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
int oldLen = dtoOptions.Fields.Count;
|
||||
var arr = new ItemFields[oldLen + 1];
|
||||
dtoOptions.Fields.CopyTo(arr, 0);
|
||||
arr[oldLen] = ItemFields.ChildCount;
|
||||
dtoOptions.Fields = arr;
|
||||
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.ChildCount];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -151,6 +151,14 @@ public class DynamicHlsHelper
|
|||
|
||||
var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
|
||||
|
||||
// from universal audio service, need to override the AudioCodec when the actual request differs from original query
|
||||
if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString());
|
||||
newQuery["AudioCodec"] = state.OutputAudioCodec;
|
||||
queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
|
||||
}
|
||||
|
||||
// from universal audio service
|
||||
if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
|
||||
&& !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase))
|
||||
|
|
|
@ -166,6 +166,9 @@ public static class StreamingHelpers
|
|||
}
|
||||
|
||||
var outputAudioCodec = streamingRequest.AudioCodec;
|
||||
state.OutputAudioCodec = outputAudioCodec;
|
||||
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
||||
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
|
||||
if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec))
|
||||
{
|
||||
state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0;
|
||||
|
@ -180,10 +183,6 @@ public static class StreamingHelpers
|
|||
containerInternal = ".pcm";
|
||||
}
|
||||
|
||||
state.OutputAudioCodec = outputAudioCodec;
|
||||
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
||||
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
|
||||
|
||||
if (state.VideoRequest is not null)
|
||||
{
|
||||
state.OutputVideoCodec = state.Request.VideoCodec;
|
||||
|
|
|
@ -106,18 +106,11 @@ public class TrickplayManager : ITrickplayManager
|
|||
}
|
||||
|
||||
var imgTempDir = string.Empty;
|
||||
var outputDir = GetTrickplayDirectory(video, width);
|
||||
|
||||
using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
|
||||
{
|
||||
_logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract images
|
||||
// Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
|
||||
var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
|
||||
|
@ -128,17 +121,35 @@ public class TrickplayManager : ITrickplayManager
|
|||
return;
|
||||
}
|
||||
|
||||
// The width has to be even, otherwise a lot of filters will not be able to sample it
|
||||
var actualWidth = 2 * (width / 2);
|
||||
|
||||
// Force using the video width when the trickplay setting has a too large width
|
||||
if (mediaSource.VideoStream.Width is not null && mediaSource.VideoStream.Width < width)
|
||||
{
|
||||
_logger.LogWarning("Video width {VideoWidth} is smaller than trickplay setting {TrickPlayWidth}, using video width for thumbnails", mediaSource.VideoStream.Width, width);
|
||||
actualWidth = 2 * ((int)mediaSource.VideoStream.Width / 2);
|
||||
}
|
||||
|
||||
var outputDir = GetTrickplayDirectory(video, actualWidth);
|
||||
|
||||
if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(actualWidth))
|
||||
{
|
||||
_logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting", video.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var mediaPath = mediaSource.Path;
|
||||
var mediaStream = mediaSource.VideoStream;
|
||||
var container = mediaSource.Container;
|
||||
|
||||
_logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
|
||||
_logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", actualWidth, mediaPath, video.Id);
|
||||
imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
|
||||
mediaPath,
|
||||
container,
|
||||
mediaSource,
|
||||
mediaStream,
|
||||
width,
|
||||
actualWidth,
|
||||
TimeSpan.FromMilliseconds(options.Interval),
|
||||
options.EnableHwAcceleration,
|
||||
options.EnableHwEncoding,
|
||||
|
@ -159,7 +170,7 @@ public class TrickplayManager : ITrickplayManager
|
|||
.ToList();
|
||||
|
||||
// Create tiles
|
||||
var trickplayInfo = CreateTiles(images, width, options, outputDir);
|
||||
var trickplayInfo = CreateTiles(images, actualWidth, options, outputDir);
|
||||
|
||||
// Save tiles info
|
||||
try
|
||||
|
|
|
@ -183,14 +183,13 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
progress.Report(percent * 95);
|
||||
}
|
||||
|
||||
// get album LUFS
|
||||
LUFS = items.OfType<Audio>().Max(item => item.LUFS);
|
||||
|
||||
var parentRefreshOptions = refreshOptions;
|
||||
if (childUpdateType > ItemUpdateType.None)
|
||||
{
|
||||
parentRefreshOptions = new MetadataRefreshOptions(refreshOptions);
|
||||
parentRefreshOptions.MetadataRefreshMode = MetadataRefreshMode.FullRefresh;
|
||||
parentRefreshOptions = new MetadataRefreshOptions(refreshOptions)
|
||||
{
|
||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh
|
||||
};
|
||||
}
|
||||
|
||||
// Refresh current item
|
||||
|
|
|
@ -135,7 +135,14 @@ namespace MediaBrowser.Controller.Entities
|
|||
/// </summary>
|
||||
/// <value>The LUFS Value.</value>
|
||||
[JsonIgnore]
|
||||
public float LUFS { get; set; }
|
||||
public float? LUFS { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the gain required for audio normalization.
|
||||
/// </summary>
|
||||
/// <value>The gain required for audio normalization.</value>
|
||||
[JsonIgnore]
|
||||
public float? NormalizationGain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channel identifier.
|
||||
|
@ -1603,7 +1610,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
|
||||
var parent = GetParents().FirstOrDefault() ?? this;
|
||||
if (parent is UserRootFolder)
|
||||
if (parent is UserRootFolder or AggregateFolder)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
@ -1772,14 +1779,11 @@ namespace MediaBrowser.Controller.Entities
|
|||
int curLen = current.Length;
|
||||
if (curLen == 0)
|
||||
{
|
||||
Studios = new[] { name };
|
||||
Studios = [name];
|
||||
}
|
||||
else
|
||||
{
|
||||
var newArr = new string[curLen + 1];
|
||||
current.CopyTo(newArr, 0);
|
||||
newArr[curLen] = name;
|
||||
Studios = newArr;
|
||||
Studios = [..current, name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1801,9 +1805,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
var genres = Genres;
|
||||
if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var list = genres.ToList();
|
||||
list.Add(name);
|
||||
Genres = list.ToArray();
|
||||
Genres = [..genres, name];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1973,12 +1975,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
public void AddImage(ItemImageInfo image)
|
||||
{
|
||||
var current = ImageInfos;
|
||||
var currentCount = current.Length;
|
||||
var newArr = new ItemImageInfo[currentCount + 1];
|
||||
current.CopyTo(newArr, 0);
|
||||
newArr[currentCount] = image;
|
||||
ImageInfos = newArr;
|
||||
ImageInfos = [..ImageInfos, image];
|
||||
}
|
||||
|
||||
public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
|
|
|
@ -30,15 +30,11 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
if (item.RemoteTrailers.Count == 0)
|
||||
{
|
||||
item.RemoteTrailers = new[] { mediaUrl };
|
||||
item.RemoteTrailers = [mediaUrl];
|
||||
}
|
||||
else
|
||||
{
|
||||
var oldIds = item.RemoteTrailers;
|
||||
var newIds = new MediaUrl[oldIds.Count + 1];
|
||||
oldIds.CopyTo(newIds);
|
||||
newIds[oldIds.Count] = mediaUrl;
|
||||
item.RemoteTrailers = newIds;
|
||||
item.RemoteTrailers = [..item.RemoteTrailers, mediaUrl];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,11 +21,11 @@ namespace MediaBrowser.Controller.Entities
|
|||
{
|
||||
if (current.Length == 0)
|
||||
{
|
||||
item.Tags = new[] { name };
|
||||
item.Tags = [name];
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Tags = current.Concat(new[] { name }).ToArray();
|
||||
item.Tags = [..current, name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,8 +116,8 @@ namespace MediaBrowser.Controller.Library
|
|||
{
|
||||
get
|
||||
{
|
||||
var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : new[] { Path };
|
||||
return AdditionalLocations is null ? paths : paths.Concat(AdditionalLocations).ToArray();
|
||||
var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : [Path];
|
||||
return AdditionalLocations is null ? paths : [..paths, ..AdditionalLocations];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2627,7 +2627,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
&& channels.Value == 2
|
||||
&& state.AudioStream is not null
|
||||
&& state.AudioStream.Channels.HasValue
|
||||
&& state.AudioStream.Channels.Value > 5)
|
||||
&& state.AudioStream.Channels.Value == 6)
|
||||
{
|
||||
switch (encodingOptions.DownMixStereoAlgorithm)
|
||||
{
|
||||
|
@ -2720,7 +2720,20 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
if (state.TranscodingType != TranscodingJobType.Progressive
|
||||
&& ((resultChannels > 2 && resultChannels < 6) || resultChannels == 7))
|
||||
{
|
||||
resultChannels = 2;
|
||||
// We can let FFMpeg supply an extra LFE channel for 5ch and 7ch to make them 5.1 and 7.1
|
||||
if (resultChannels == 5)
|
||||
{
|
||||
resultChannels = 6;
|
||||
}
|
||||
else if (resultChannels == 7)
|
||||
{
|
||||
resultChannels = 8;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For other weird layout, just downmix to stereo for compatibility
|
||||
resultChannels = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2971,8 +2984,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
var scaleW = (double)maximumWidth / outputWidth;
|
||||
var scaleH = (double)maximumHeight / outputHeight;
|
||||
var scale = Math.Min(scaleW, scaleH);
|
||||
outputWidth = Math.Min(maximumWidth, (int)(outputWidth * scale));
|
||||
outputHeight = Math.Min(maximumHeight, (int)(outputHeight * scale));
|
||||
outputWidth = Math.Min(maximumWidth, Convert.ToInt32(outputWidth * scale));
|
||||
outputHeight = Math.Min(maximumHeight, Convert.ToInt32(outputHeight * scale));
|
||||
}
|
||||
|
||||
outputWidth = 2 * (outputWidth / 2);
|
||||
|
@ -6903,7 +6916,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
if (channels.HasValue && ((channels.Value != 2 && state.AudioStream.Channels <= 5) || encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None))
|
||||
if (channels.HasValue && ((channels.Value != 2 && state.AudioStream?.Channels != 6) || encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None))
|
||||
{
|
||||
args += " -ac " + channels.Value;
|
||||
}
|
||||
|
|
|
@ -288,7 +288,7 @@ namespace MediaBrowser.Controller.Net
|
|||
|
||||
lock (_activeConnectionsLock)
|
||||
{
|
||||
foreach (var connection in _activeConnections.ToArray())
|
||||
foreach (var connection in _activeConnections.ToList())
|
||||
{
|
||||
DisposeConnection(connection);
|
||||
}
|
||||
|
|
|
@ -270,9 +270,7 @@ namespace MediaBrowser.Controller.Session
|
|||
|
||||
public void AddController(ISessionController controller)
|
||||
{
|
||||
var controllers = SessionControllers.ToList();
|
||||
controllers.Add(controller);
|
||||
SessionControllers = controllers.ToArray();
|
||||
SessionControllers = [..SessionControllers, controller];
|
||||
}
|
||||
|
||||
public bool ContainsUser(Guid userId)
|
||||
|
|
|
@ -1342,9 +1342,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
return null;
|
||||
}
|
||||
|
||||
return value.Split('/', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(i => i.Trim())
|
||||
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
|
||||
return value.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1353,17 +1352,13 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
/// <param name="val">The val.</param>
|
||||
/// <param name="allowCommaDelimiter">if set to <c>true</c> [allow comma delimiter].</param>
|
||||
/// <returns>System.String[][].</returns>
|
||||
private IEnumerable<string> Split(string val, bool allowCommaDelimiter)
|
||||
private string[] Split(string val, bool allowCommaDelimiter)
|
||||
{
|
||||
// Only use the comma as a delimiter if there are no slashes or pipes.
|
||||
// We want to be careful not to split names that have commas in them
|
||||
var delimiter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.Contains(i, StringComparison.Ordinal)) ?
|
||||
_nameDelimiters :
|
||||
new[] { ',' };
|
||||
|
||||
return val.Split(delimiter, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim());
|
||||
return !allowCommaDelimiter || _nameDelimiters.Any(i => val.Contains(i, StringComparison.Ordinal)) ?
|
||||
val.Split(_nameDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) :
|
||||
val.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
private IEnumerable<string> SplitDistinctArtists(string val, char[] delimiters, bool splitFeaturing)
|
||||
|
@ -1387,9 +1382,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
}
|
||||
}
|
||||
|
||||
var artists = val.Split(delimiters, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim());
|
||||
var artists = val.Split(delimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
artistsFound.AddRange(artists);
|
||||
return artistsFound.DistinctNames();
|
||||
|
@ -1514,15 +1507,12 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
|
||||
if (tags.TryGetValue("WM/Genre", out var genres) && !string.IsNullOrWhiteSpace(genres))
|
||||
{
|
||||
var genreList = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim())
|
||||
.ToList();
|
||||
var genreList = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
// If this is empty then don't overwrite genres that might have been fetched earlier
|
||||
if (genreList.Count > 0)
|
||||
if (genreList.Length > 0)
|
||||
{
|
||||
video.Genres = genreList.ToArray();
|
||||
video.Genres = genreList;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1533,10 +1523,9 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
|
||||
if (tags.TryGetValue("WM/MediaCredits", out var people) && !string.IsNullOrEmpty(people))
|
||||
{
|
||||
video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonKind.Actor })
|
||||
.ToArray();
|
||||
video.People = Array.ConvertAll(
|
||||
people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries),
|
||||
i => new BaseItemPerson { Name = i, Type = PersonKind.Actor });
|
||||
}
|
||||
|
||||
if (tags.TryGetValue("WM/OriginalReleaseTime", out var year) && int.TryParse(year, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
|
||||
|
|
|
@ -479,6 +479,11 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
: "FFmpeg.DirectStream-";
|
||||
}
|
||||
|
||||
if (state.VideoRequest is null && EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
|
||||
{
|
||||
logFilePrefix = "FFmpeg.Remux-";
|
||||
}
|
||||
|
||||
var logFilePath = Path.Combine(
|
||||
_serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
|
||||
$"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
|
||||
|
|
|
@ -35,8 +35,6 @@ namespace MediaBrowser.Model.Configuration
|
|||
|
||||
public bool EnableLUFSScan { get; set; }
|
||||
|
||||
public bool UseReplayGainTags { get; set; }
|
||||
|
||||
public bool EnableChapterImageExtraction { get; set; }
|
||||
|
||||
public bool ExtractChapterImagesDuringLibraryScan { get; set; }
|
||||
|
|
|
@ -108,7 +108,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
var inputAudioSampleRate = audioStream?.SampleRate;
|
||||
var inputAudioBitDepth = audioStream?.BitDepth;
|
||||
|
||||
if (directPlayMethod.HasValue)
|
||||
if (directPlayMethod is PlayMethod.DirectPlay)
|
||||
{
|
||||
var profile = options.Profile;
|
||||
var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true);
|
||||
|
@ -124,6 +124,46 @@ namespace MediaBrowser.Model.Dlna
|
|||
}
|
||||
}
|
||||
|
||||
if (directPlayMethod is PlayMethod.DirectStream)
|
||||
{
|
||||
var remuxContainer = item.TranscodingContainer ?? "ts";
|
||||
var supportedHlsContainers = new[] { "ts", "mp4" };
|
||||
// If the container specified for the profile is an HLS supported container, use that container instead, overriding the preference
|
||||
// The client should be responsible to ensure this container is compatible
|
||||
remuxContainer = Array.Exists(supportedHlsContainers, element => element == directPlayInfo.Profile?.Container) ? directPlayInfo.Profile?.Container : remuxContainer;
|
||||
bool codeIsSupported;
|
||||
if (item.TranscodingSubProtocol == MediaStreamProtocol.hls)
|
||||
{
|
||||
// Enforce HLS audio codec restrictions
|
||||
if (string.Equals(remuxContainer, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
codeIsSupported = _supportedHlsAudioCodecsMp4.Contains(directPlayInfo.Profile?.AudioCodec ?? directPlayInfo.Profile?.Container);
|
||||
}
|
||||
else
|
||||
{
|
||||
codeIsSupported = _supportedHlsAudioCodecsTs.Contains(directPlayInfo.Profile?.AudioCodec ?? directPlayInfo.Profile?.Container);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Let's assume the client has given a correct container for http
|
||||
codeIsSupported = true;
|
||||
}
|
||||
|
||||
if (codeIsSupported)
|
||||
{
|
||||
playlistItem.PlayMethod = directPlayMethod.Value;
|
||||
playlistItem.Container = remuxContainer;
|
||||
playlistItem.TranscodeReasons = transcodeReasons;
|
||||
playlistItem.SubProtocol = item.TranscodingSubProtocol;
|
||||
item.TranscodingContainer = remuxContainer;
|
||||
return playlistItem;
|
||||
}
|
||||
|
||||
transcodeReasons |= TranscodeReason.AudioCodecNotSupported;
|
||||
playlistItem.TranscodeReasons = transcodeReasons;
|
||||
}
|
||||
|
||||
TranscodingProfile? transcodingProfile = null;
|
||||
foreach (var tcProfile in options.Profile.TranscodingProfiles)
|
||||
{
|
||||
|
@ -387,6 +427,14 @@ namespace MediaBrowser.Model.Dlna
|
|||
item.Path ?? "Unknown path",
|
||||
audioStream.Codec ?? "Unknown codec");
|
||||
|
||||
var directStreamProfile = options.Profile.DirectPlayProfiles
|
||||
.FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectStreamSupported(x, item, audioStream));
|
||||
|
||||
if (directStreamProfile is not null)
|
||||
{
|
||||
return (directStreamProfile, PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported);
|
||||
}
|
||||
|
||||
return (null, null, GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
|
||||
}
|
||||
|
||||
|
@ -2129,5 +2177,23 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsAudioDirectStreamSupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
|
||||
{
|
||||
// Check container type, this should NOT be supported
|
||||
if (!profile.SupportsContainer(item.Container))
|
||||
{
|
||||
// Check audio codec, we cannot use the SupportsAudioCodec here
|
||||
// Because that one assumes empty container supports all codec, which is just useless
|
||||
string? audioCodec = audioStream?.Codec;
|
||||
if (string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -782,10 +782,10 @@ namespace MediaBrowser.Model.Dto
|
|||
public string TimerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LUFS value.
|
||||
/// Gets or sets the gain required for audio normalization.
|
||||
/// </summary>
|
||||
/// <value>The LUFS Value.</value>
|
||||
public float? LUFS { get; set; }
|
||||
/// <value>The gain required for audio normalization.</value>
|
||||
public float? NormalizationGain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current program.
|
||||
|
|
|
@ -286,7 +286,7 @@ namespace MediaBrowser.Providers.Manager
|
|||
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
return results.SelectMany(i => i.ToList());
|
||||
return results.SelectMany(i => i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
|
@ -18,7 +15,6 @@ using MediaBrowser.Model.Dlna;
|
|||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TagLib;
|
||||
|
||||
namespace MediaBrowser.Providers.MediaInfo
|
||||
|
@ -26,12 +22,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
/// <summary>
|
||||
/// Probes audio files for metadata.
|
||||
/// </summary>
|
||||
public partial class AudioFileProber
|
||||
public class AudioFileProber
|
||||
{
|
||||
// Default LUFS value for use with the web interface, at -18db gain will be 1(no db gain).
|
||||
private const float DefaultLUFSValue = -18;
|
||||
|
||||
private readonly ILogger<AudioFileProber> _logger;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
@ -42,7 +34,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioFileProber"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
/// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
|
@ -50,7 +41,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
/// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
|
||||
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
|
||||
public AudioFileProber(
|
||||
ILogger<AudioFileProber> logger,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IItemRepository itemRepo,
|
||||
|
@ -58,7 +48,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
LyricResolver lyricResolver,
|
||||
ILyricManager lyricManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_itemRepo = itemRepo;
|
||||
_libraryManager = libraryManager;
|
||||
|
@ -67,12 +56,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
_lyricManager = lyricManager;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
|
||||
private static partial Regex LUFSRegex();
|
||||
|
||||
[GeneratedRegex(@"REPLAYGAIN_TRACK_GAIN:\s+-?([0-9.]+)\s+dB")]
|
||||
private static partial Regex ReplayGainTagRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Probes the specified item for metadata.
|
||||
/// </summary>
|
||||
|
@ -115,97 +98,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
await FetchAsync(item, result, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var libraryOptions = _libraryManager.GetLibraryOptions(item);
|
||||
bool foundLUFSValue = false;
|
||||
|
||||
if (libraryOptions.UseReplayGainTags)
|
||||
{
|
||||
using (var process = new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = _mediaEncoder.ProbePath,
|
||||
Arguments = $"-hide_banner -i \"{path}\"",
|
||||
RedirectStandardOutput = false,
|
||||
RedirectStandardError = true
|
||||
},
|
||||
})
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting ffmpeg");
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
using var reader = process.StandardError;
|
||||
var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Match split = ReplayGainTagRegex().Match(output);
|
||||
|
||||
if (split.Success)
|
||||
{
|
||||
item.LUFS = DefaultLUFSValue - float.Parse(split.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||
foundLUFSValue = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.LUFS = DefaultLUFSValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (libraryOptions.EnableLUFSScan && !foundLUFSValue)
|
||||
{
|
||||
using (var process = new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = $"-hide_banner -i \"{path}\" -af ebur128=framelog=verbose -f null -",
|
||||
RedirectStandardOutput = false,
|
||||
RedirectStandardError = true
|
||||
},
|
||||
})
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting ffmpeg");
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
using var reader = process.StandardError;
|
||||
var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
MatchCollection split = LUFSRegex().Matches(output);
|
||||
|
||||
if (split.Count != 0)
|
||||
{
|
||||
item.LUFS = float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.LUFS = DefaultLUFSValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!libraryOptions.EnableLUFSScan && !libraryOptions.UseReplayGainTags)
|
||||
{
|
||||
item.LUFS = DefaultLUFSValue;
|
||||
}
|
||||
|
||||
_logger.LogDebug("LUFS for {ItemName} is {LUFS}.", item.Name, item.LUFS);
|
||||
|
||||
return ItemUpdateType.MetadataImport;
|
||||
}
|
||||
|
||||
|
@ -232,7 +124,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|
||||
if (!audio.IsLocked)
|
||||
{
|
||||
await FetchDataFromTags(audio, options).ConfigureAwait(false);
|
||||
await FetchDataFromTags(audio, mediaInfo, options).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams);
|
||||
|
@ -247,8 +139,9 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
/// Fetches data from the tags.
|
||||
/// </summary>
|
||||
/// <param name="audio">The <see cref="Audio"/>.</param>
|
||||
/// <param name="mediaInfo">The <see cref="Model.MediaInfo.MediaInfo"/>.</param>
|
||||
/// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
|
||||
private async Task FetchDataFromTags(Audio audio, MetadataRefreshOptions options)
|
||||
private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options)
|
||||
{
|
||||
using var file = TagLib.File.Create(audio.Path);
|
||||
var tagTypes = file.TagTypesOnDisk;
|
||||
|
@ -391,6 +284,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
: audio.Genres;
|
||||
}
|
||||
|
||||
if (!double.IsNaN(tags.ReplayGainTrackGain))
|
||||
{
|
||||
audio.NormalizationGain = (float)tags.ReplayGainTrackGain;
|
||||
}
|
||||
|
||||
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
|
||||
{
|
||||
audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId);
|
||||
|
@ -415,7 +313,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
{
|
||||
// Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`.
|
||||
// See https://github.com/mono/taglib-sharp/issues/304
|
||||
var mediaInfo = await GetMediaInfo(audio, CancellationToken.None).ConfigureAwait(false);
|
||||
var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack);
|
||||
if (trackMbId is not null)
|
||||
{
|
||||
|
@ -444,20 +341,5 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
|
||||
currentStreams.AddRange(externalLyricFiles);
|
||||
}
|
||||
|
||||
private async Task<Model.MediaInfo.MediaInfo> GetMediaInfo(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new MediaInfoRequest
|
||||
{
|
||||
MediaType = DlnaProfileType.Audio,
|
||||
MediaSource = new MediaSourceInfo
|
||||
{
|
||||
Path = item.Path,
|
||||
Protocol = item.PathProtocol ?? MediaProtocol.File
|
||||
}
|
||||
};
|
||||
|
||||
return await _mediaEncoder.GetMediaInfo(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,7 +103,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
_subtitleResolver);
|
||||
|
||||
_audioProber = new AudioFileProber(
|
||||
loggerFactory.CreateLogger<AudioFileProber>(),
|
||||
mediaSourceManager,
|
||||
mediaEncoder,
|
||||
itemRepo,
|
||||
|
|
|
@ -447,11 +447,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
|||
var actorList = result.Actors.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
foreach (var actor in actorList)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(actor))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var person = new PersonInfo
|
||||
{
|
||||
Name = actor,
|
||||
|
|
|
@ -117,9 +117,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||
var artist = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo)
|
||||
{
|
||||
var list = artistVideo.Artists.ToList();
|
||||
list.Add(artist);
|
||||
artistVideo.Artists = list.ToArray();
|
||||
artistVideo.Artists = [..artistVideo.Artists, artist];
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
|
@ -1130,7 +1130,7 @@ namespace Jellyfin.LiveTv.Channels
|
|||
{
|
||||
if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
|
||||
item.Tags = [..item.Tags, "livestream"];
|
||||
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
|
||||
forceUpdate = true;
|
||||
}
|
||||
|
|
|
@ -60,14 +60,13 @@ public class ListingsManager : IListingsManager
|
|||
|
||||
var config = _config.GetLiveTvConfiguration();
|
||||
|
||||
var list = config.ListingProviders.ToList();
|
||||
int index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
|
||||
var list = config.ListingProviders;
|
||||
int index = Array.FindIndex(list, i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
|
||||
{
|
||||
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
list.Add(info);
|
||||
config.ListingProviders = list.ToArray();
|
||||
config.ListingProviders = [..list, info];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -236,13 +235,12 @@ public class ListingsManager : IListingsManager
|
|||
|
||||
if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var list = listingsProviderInfo.ChannelMappings.ToList();
|
||||
list.Add(new NameValuePair
|
||||
var newItem = new NameValuePair
|
||||
{
|
||||
Name = tunerChannelNumber,
|
||||
Value = providerChannelNumber
|
||||
});
|
||||
listingsProviderInfo.ChannelMappings = list.ToArray();
|
||||
};
|
||||
listingsProviderInfo.ChannelMappings = [..listingsProviderInfo.ChannelMappings, newItem];
|
||||
}
|
||||
|
||||
_config.SaveConfiguration("livetv", config);
|
||||
|
|
|
@ -939,7 +939,7 @@ namespace Jellyfin.LiveTv
|
|||
{
|
||||
var internalChannelId = _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId);
|
||||
var channel = _libraryManager.GetItemById(internalChannelId);
|
||||
channelName = channel is null ? null : channel.Name;
|
||||
channelName = channel?.Name;
|
||||
}
|
||||
|
||||
return _tvDtoService.GetSeriesTimerInfoDto(i.Item1, i.Item2, channelName);
|
||||
|
|
|
@ -115,11 +115,7 @@ namespace Jellyfin.LiveTv.Timers
|
|||
throw new ArgumentException("item already exists", nameof(item));
|
||||
}
|
||||
|
||||
int oldLen = _items.Length;
|
||||
var newList = new T[oldLen + 1];
|
||||
_items.CopyTo(newList, 0);
|
||||
newList[oldLen] = item;
|
||||
_items = newList;
|
||||
_items = [.._items, item];
|
||||
|
||||
SaveList();
|
||||
}
|
||||
|
@ -134,11 +130,7 @@ namespace Jellyfin.LiveTv.Timers
|
|||
int index = Array.FindIndex(_items, i => EqualityComparer(i, item));
|
||||
if (index == -1)
|
||||
{
|
||||
int oldLen = _items.Length;
|
||||
var newList = new T[oldLen + 1];
|
||||
_items.CopyTo(newList, 0);
|
||||
newList[oldLen] = item;
|
||||
_items = newList;
|
||||
_items = [.._items, item];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -273,12 +273,12 @@ namespace Jellyfin.LiveTv.TunerHosts
|
|||
var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal);
|
||||
if (numberIndex > 0)
|
||||
{
|
||||
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
|
||||
var numberPart = nameInExtInf.AsSpan(0, numberIndex).Trim(new[] { ' ', '.' });
|
||||
|
||||
if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
// channel.Number = number.ToString();
|
||||
nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
|
||||
nameInExtInf = nameInExtInf.AsSpan(numberIndex + 1).Trim(new[] { ' ', '-' }).ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,14 +76,13 @@ public class TunerHostManager : ITunerHostManager
|
|||
|
||||
var config = _config.GetLiveTvConfiguration();
|
||||
|
||||
var list = config.TunerHosts.ToList();
|
||||
var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
|
||||
var list = config.TunerHosts;
|
||||
var index = Array.FindIndex(list, i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
|
||||
{
|
||||
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
list.Add(info);
|
||||
config.TunerHosts = list.ToArray();
|
||||
config.TunerHosts = [..list, info];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue