Compare commits

...

47 commits

Author SHA1 Message Date
gnattu 8497e8d695 Fix direct play
The SupportsDirectStream is a little bit misleading as it actually means "Supports Direct Play"

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-05 11:21:10 +08:00
gnattu b258e85f69 Allow clients to send audio container override for HLS
This will improve flexibility due to overcome the complex compatibility situation of HLS

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-04 23:18:46 +08:00
gnattu eb22c222a0 feat: add audio remux to UniversalAudioController
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-04 23:18:46 +08:00
Andi Chandler f453746e2c Translated using Weblate (English (United Kingdom))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en_GB/
2024-05-03 17:52:55 -04:00
HanHwanHo 169698619d Translated using Weblate (Korean)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ko/
2024-05-03 10:40:23 -04:00
Blackspirits 34a8cc203a Translated using Weblate (Portuguese)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/
2024-05-02 18:35:52 -04:00
Blackspirits bd3fdcd1d4 Translated using Weblate (Portuguese (Portugal))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/
2024-05-02 18:35:52 -04:00
stanol 45d9481f73 Translated using Weblate (Ukrainian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uk/
2024-05-02 12:18:24 -04:00
nextlooper42 4a4540bb0f Translated using Weblate (Slovak)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sk/
2024-05-02 09:37:48 -04:00
bene toffix f20a9c9b2b Translated using Weblate (Catalan)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/
2024-05-01 12:10:05 -04:00
Cody Robibero 1accfd79da
Always attempt to get User if a user id is provided (#11471) 2024-05-01 06:42:01 -06:00
Bas 1b4f5451a5 Translated using Weblate (Dutch)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/
2024-05-01 08:31:23 -04:00
Kityn 41fd7f168b Translated using Weblate (Polish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pl/
2024-05-01 04:16:51 -04:00
Lukáš Kucharczyk 6633c26f46 Translated using Weblate (Czech)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/
2024-05-01 04:16:51 -04:00
myrad2267 16b2483d84 Translated using Weblate (French)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr/
2024-04-30 23:10:34 -04:00
Rany a1f5e7265f Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/
2024-04-30 17:37:03 -04:00
Bond-009 3feb3f81bf
More efficient array creation (#11468) 2024-04-30 13:32:59 -06:00
gnattu 5dc6bb4910
Fix incomplete tag query for whitelist tags (#11416) 2024-04-30 13:32:49 -06:00
Cody Robibero 48bb16472f
Merge pull request #11457 from Bond-009/audionormalization 2024-04-30 13:15:51 -06:00
Bond-009 74f3e54807
Merge pull request #11436 from nielsvanvelzen/plugin-api-elevation
Require elevation for plugin related endpoints
2024-04-30 20:32:01 +02:00
Bond_009 9ffb07d67f Fix filename 2024-04-30 16:16:42 +02:00
Bond_009 8c9d0df7f2 Address comments 2024-04-30 16:14:01 +02:00
gnattu 6f78ac2ff3
Use more accurate rounding in GetFixedOutputSize (#11435)
* Use more accurate rounding in GetFixedOutputSize

Signed-off-by: gnattu <gnattuoc@me.com>

* Force trickplay thumbnails to have even width

Signed-off-by: gnattu <gnattuoc@me.com>

* Use Convert.ToInt32

Signed-off-by: gnattu <gnattuoc@me.com>

* Force video size as thumbnail size if the trickplay width setting is larger

This will fix an issue when the trickplay setting contains a very huge width, but the video has a lower resolution than that setting. Our scaling filter logic will not do any upscale, and we have to force to use the video width

Signed-off-by: gnattu <gnattuoc@me.com>

---------

Signed-off-by: gnattu <gnattuoc@me.com>
2024-04-30 13:41:46 +02:00
Niels van Velzen 0b15352771
Merge pull request #11361 from Bond-009/nope
Properly await Task.Delay()
2024-04-30 13:41:13 +02:00
Bond_009 276ae3b8b7 Skip albums that don't have multiple tracks 2024-04-29 14:50:46 +02:00
Bond-009 2cbef3aa38
Merge pull request #11440 from jellyfin/renovate/ci-deps
chore(deps): update github/codeql-action action to v3.25.3
2024-04-29 10:17:25 +02:00
Bond_009 2459b7e62e Properly await Task.Delay() 2024-04-29 10:16:28 +02:00
Bond_009 2ad872001d Address comments 2024-04-28 17:16:33 +02:00
Roots Radics 8eff5d1bf7 Translated using Weblate (French (Canada))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr_CA/
2024-04-28 10:03:08 -04:00
Bond_009 88a38a61b5 Improve audio normalization
* Move calculation of LUFS to a scheduled task as it's pretty slow
* Correctly calculate album LUFS
* Don't try to convert replaygain tags to LUFS values
2024-04-28 15:18:53 +02:00
Niels van Velzen 935c2c97fe Require elevation for plugin related endpoints 2024-04-26 19:00:53 +02:00
renovate[bot] f63148441d
chore(deps): update github/codeql-action action to v3.25.3 2024-04-26 02:12:37 +00:00
renovate[bot] 5612cb8178
chore(deps): update ci dependencies (#11427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-25 07:15:31 -06:00
renovate[bot] ccd06bc547
chore(deps): update dependency diacritics to v3.3.29 (#11429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-25 07:02:45 -06:00
Bond-009 d29b85a134
Fix multiple intro providers and remove unneeded ToLists (#11431) 2024-04-25 07:02:01 -06:00
renovate[bot] 9a515149ef
chore(deps): update danielpalme/reportgenerator-github-action action to v5.2.5 (#11423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-24 08:43:29 -06:00
Bond-009 ac108690a8
Use StringSplitOptions.TrimEntries where possible (#11421) 2024-04-24 08:35:15 -06:00
Bond-009 428283f787
Always scan ReplayGain tag (#11418) 2024-04-24 08:09:01 -06:00
alison2033 3c159822b5 Translated using Weblate (Portuguese (Brazil))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/
2024-04-23 21:00:27 -04:00
AKHI f5f0d18934 Translated using Weblate (Malayalam)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ml/
2024-04-23 16:45:49 -04:00
Bond-009 3936fc9f25
Don't run ffprobe a second time for music file (#11419) 2024-04-23 07:08:49 -06:00
John M c791331952
Fix task CleanupCollectionAndPlaylistPathsTask removing valid paths (#11410) 2024-04-23 07:08:19 -06:00
Bond-009 f26eb15be0
Merge pull request #11405 from jellyfin/renovate/ci-deps
chore(deps): update ci dependencies
2024-04-23 11:35:03 +02:00
renovate[bot] bafb7f84ad
chore(deps): update ci dependencies 2024-04-23 02:18:53 +00:00
gnattu 374b6ca0e2
Only apply custom downmix to 5.1 audios (#11401) 2024-04-22 10:23:36 -06:00
renovate[bot] 09b0229670
chore(deps): update actions/checkout action to v4.1.3 (#11404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-22 10:23:08 -06:00
renovate[bot] ab55dcf82d
chore(deps): update dependency efcoresecondlevelcacheinterceptor to v4.4.3 (#11402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-22 10:22:50 -06:00
75 changed files with 556 additions and 399 deletions

View file

@ -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

View file

@ -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

View file

@ -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/"

View file

@ -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

View file

@ -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

View file

@ -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 }}

View file

@ -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" />

View file

@ -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

View file

@ -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
{

View file

@ -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)

View file

@ -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.

View file

@ -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);
}

View file

@ -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)

View file

@ -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;

View file

@ -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))
{

View file

@ -126,5 +126,7 @@
"External": "خارجي",
"HearingImpaired": "ضعاف السمع",
"TaskRefreshTrickplayImages": "توليد صور Trickplay",
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة."
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة."
}

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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",

View file

@ -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"
}

View file

@ -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."
}

View file

@ -124,5 +124,6 @@
"TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.",
"TaskKeyframeExtractor": "키프레임 추출",
"External": "외부",
"HearingImpaired": "청각 장애"
"HearingImpaired": "청각 장애",
"TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리"
}

View file

@ -123,5 +123,7 @@
"HearingImpaired": "കേൾവി തകരാറുകൾ",
"External": "പുറമേയുള്ള",
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ",
"TaskCleanCollectionsAndPlaylistsDescription": "നിലവിലില്ലാത്ത ശേഖരങ്ങളിൽ നിന്നും പ്ലേലിസ്റ്റുകളിൽ നിന്നും ഇനങ്ങൾ നീക്കംചെയ്യുന്നു.",
"TaskCleanCollectionsAndPlaylists": "ശേഖരങ്ങളും പ്ലേലിസ്റ്റുകളും വൃത്തിയാക്കുക"
}

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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."
}

View file

@ -127,5 +127,7 @@
"TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
"TaskRefreshTrickplayImages": "Створити Trickplay-зображення",
"TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення",
"TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують."
"TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують.",
"TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.",
"TaskAudioNormalization": "Нормалізація аудіо"
}

View file

@ -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();

View file

@ -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();
}

View file

@ -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;
}
}
}

View file

@ -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;

View file

@ -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);

View file

@ -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];
}
}

View file

@ -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)
{

View file

@ -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)
{

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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);

View file

@ -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)
{

View file

@ -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.")]

View file

@ -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,

View file

@ -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);
}

View file

@ -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];
}
}

View file

@ -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))

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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];
}
}
}

View file

@ -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];
}
}
}

View file

@ -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];
}
}

View file

@ -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;
}

View file

@ -288,7 +288,7 @@ namespace MediaBrowser.Controller.Net
lock (_activeConnectionsLock)
{
foreach (var connection in _activeConnections.ToArray())
foreach (var connection in _activeConnections.ToList())
{
DisposeConnection(connection);
}

View file

@ -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)

View file

@ -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))

View file

@ -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");

View file

@ -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; }

View file

@ -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;
}
}
}

View file

@ -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.

View file

@ -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>

View file

@ -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);
}
}
}

View file

@ -103,7 +103,6 @@ namespace MediaBrowser.Providers.MediaInfo
_subtitleResolver);
_audioProber = new AudioFileProber(
loggerFactory.CreateLogger<AudioFileProber>(),
mediaSourceManager,
mediaEncoder,
itemRepo,

View file

@ -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,

View file

@ -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;

View file

@ -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;
}

View file

@ -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);

View file

@ -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);

View file

@ -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
{

View file

@ -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();
}
}
}

View file

@ -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
{