mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-02-06 20:52:23 +00:00
Compare commits
85 Commits
v10.11.0-r
...
v10.11.0-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
982e0c9370 | ||
|
|
55e681b9a6 | ||
|
|
7ba77804c4 | ||
|
|
af6f5a8ed0 | ||
|
|
1162fcebf8 | ||
|
|
38d0367c42 | ||
|
|
7d3372018f | ||
|
|
8629831658 | ||
|
|
db55d983f8 | ||
|
|
4d5ba8d7a5 | ||
|
|
6d4169a449 | ||
|
|
8dcb0bfecb | ||
|
|
844d69ab64 | ||
|
|
5c36b44484 | ||
|
|
4e4d7e7764 | ||
|
|
4c268a3579 | ||
|
|
77bcd2f5f6 | ||
|
|
8406924471 | ||
|
|
67fd4ce187 | ||
|
|
b37b39773a | ||
|
|
6f98767aed | ||
|
|
643460f484 | ||
|
|
a4231bf428 | ||
|
|
9c817a97a9 | ||
|
|
dde306b170 | ||
|
|
e2b61d951b | ||
|
|
9eff25bfed | ||
|
|
ff4484eb4a | ||
|
|
62b2adbf66 | ||
|
|
9ac8c2a2fa | ||
|
|
90e72fb687 | ||
|
|
630846798d | ||
|
|
9d5be19a27 | ||
|
|
6058ab50f8 | ||
|
|
e3b379052d | ||
|
|
0b6f4b2bd9 | ||
|
|
4f6db1bc22 | ||
|
|
8c8c71125c | ||
|
|
c6e568692e | ||
|
|
d5a76bdff8 | ||
|
|
ebdc756547 | ||
|
|
10d0cec7b9 | ||
|
|
10cc651790 | ||
|
|
7d18f3d6ed | ||
|
|
9b8c12d433 | ||
|
|
ba0eb87371 | ||
|
|
d561cef81f | ||
|
|
b528c1100f | ||
|
|
96c9f4fdad | ||
|
|
6d077fcf40 | ||
|
|
ab99b2bad3 | ||
|
|
db36be7a6b | ||
|
|
85f158e1dd | ||
|
|
e1365bd253 | ||
|
|
1ec66adc30 | ||
|
|
af0bcbc652 | ||
|
|
b2312466e1 | ||
|
|
cc7915c2e6 | ||
|
|
a537c66da1 | ||
|
|
a43adf42f3 | ||
|
|
6996c8a1de | ||
|
|
f976630003 | ||
|
|
965cf93419 | ||
|
|
70ea3f863a | ||
|
|
989aef18af | ||
|
|
ccb917b8df | ||
|
|
7cf6389ab5 | ||
|
|
2473b89a8d | ||
|
|
6575c69a4e | ||
|
|
66d594836c | ||
|
|
43028f735f | ||
|
|
e83b992eef | ||
|
|
8368d10d1b | ||
|
|
e8291fc856 | ||
|
|
308707476d | ||
|
|
e252589900 | ||
|
|
1220cac255 | ||
|
|
7218d82c21 | ||
|
|
a4524eb2ad | ||
|
|
553ba56389 | ||
|
|
afa2103d42 | ||
|
|
7256c9c89d | ||
|
|
f3cdaeaa12 | ||
|
|
368808eba4 | ||
|
|
0fc8ed6aeb |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.6",
|
||||
"version": "9.0.7",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"dotnetRuntimeVersions": "9.0",
|
||||
"aspNetCoreRuntimeVersions": "9.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
|
||||
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
|
||||
"preserve_apt_list": false,
|
||||
"packages": [
|
||||
"libfontconfig1"
|
||||
|
||||
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -27,11 +27,11 @@ jobs:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
|
||||
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@c9576654e2fea2faa7b69e59550b3805bf6a9977 # v5.4.7
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@4c0f60daf67483745c34efdeadd4c4e78a19991e # v5.4.8
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
- [revam](https://github.com/revam)
|
||||
- [allesmi](https://github.com/allesmi)
|
||||
- [ThunderClapLP](https://github.com/ThunderClapLP)
|
||||
- [Shoham Peller](https://github.com/spellr)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||
<PackageVersion Include="BDInfo" Version="0.8.0" />
|
||||
<PackageVersion Include="BitFaster.Caching" Version="2.5.3" />
|
||||
<PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.29" />
|
||||
<PackageVersion Include="Diacritics" Version="4.0.14" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="3.3.0" />
|
||||
@@ -24,31 +24,31 @@
|
||||
<PackageVersion Include="Ignore" Version="0.2.1" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="4.0.12" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.228.1" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
||||
@@ -59,9 +59,10 @@
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="Polly" Version="8.6.0" />
|
||||
<PackageVersion Include="Polly" Version="8.6.2" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
@@ -79,12 +80,12 @@
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.6" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.6" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.6" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.7" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="6.26.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.0.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
@@ -92,4 +93,4 @@
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -188,7 +188,8 @@ namespace Emby.Naming.Common
|
||||
"disk",
|
||||
"vol",
|
||||
"volume",
|
||||
"part"
|
||||
"part",
|
||||
"act"
|
||||
};
|
||||
|
||||
ArtistSubfolders = new[]
|
||||
|
||||
@@ -108,7 +108,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
|
||||
var dateTaken = image.ImageTag.DateTime;
|
||||
if (dateTaken.HasValue)
|
||||
{
|
||||
item.DateCreated = dateTaken.Value;
|
||||
item.DateCreated = dateTaken.Value.ToUniversalTime();
|
||||
item.PremiereDate = dateTaken.Value;
|
||||
item.ProductionYear = dateTaken.Value.Year;
|
||||
}
|
||||
|
||||
@@ -1065,7 +1065,12 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.Trickplay))
|
||||
{
|
||||
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
||||
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
||||
dto.Trickplay = trickplay.ToDictionary(
|
||||
mediaStream => mediaStream.Key,
|
||||
mediaStream => mediaStream.Value.ToDictionary(
|
||||
width => width.Key,
|
||||
width => new TrickplayInfoDto(width.Value)));
|
||||
}
|
||||
|
||||
dto.ExtraType = video.ExtraType;
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
|
||||
_jsonOptions = JsonDefaults.Options;
|
||||
LastActivityDate = DateTime.Now;
|
||||
LastActivityDate = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -43,13 +43,11 @@ namespace Emby.Server.Implementations.Images
|
||||
protected IImageProcessor ImageProcessor { get; set; }
|
||||
|
||||
protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
|
||||
= new ImageType[] { ImageType.Primary };
|
||||
= [ImageType.Primary];
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Dynamic Image Provider";
|
||||
|
||||
protected virtual int MaxImageAgeDays => 7;
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
protected virtual bool Supports(BaseItem item) => true;
|
||||
@@ -292,8 +290,14 @@ namespace Emby.Server.Implementations.Images
|
||||
|
||||
protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image)
|
||||
{
|
||||
var age = DateTime.UtcNow - image.DateModified;
|
||||
return age.TotalDays > MaxImageAgeDays;
|
||||
var path = image.Path;
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var modificationDate = FileSystem.GetLastWriteTimeUtc(path);
|
||||
return image.DateModified != modificationDate;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)
|
||||
|
||||
@@ -1981,6 +1981,8 @@ namespace Emby.Server.Implementations.Library
|
||||
return;
|
||||
}
|
||||
|
||||
var anyChange = false;
|
||||
|
||||
foreach (var img in outdated)
|
||||
{
|
||||
var image = img;
|
||||
@@ -2012,6 +2014,7 @@ namespace Emby.Server.Implementations.Library
|
||||
try
|
||||
{
|
||||
size = _imageProcessor.GetImageDimensions(item, image);
|
||||
anyChange = image.Width != size.Width || image.Height != size.Height;
|
||||
image.Width = size.Width;
|
||||
image.Height = size.Height;
|
||||
}
|
||||
@@ -2019,23 +2022,29 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
||||
size = default;
|
||||
anyChange = image.Width != size.Width || image.Height != size.Height;
|
||||
image.Width = 0;
|
||||
image.Height = 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
|
||||
var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
|
||||
anyChange = anyChange || !blurhash.Equals(image.BlurHash, StringComparison.Ordinal);
|
||||
image.BlurHash = blurhash;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
|
||||
anyChange = anyChange || !string.IsNullOrEmpty(image.BlurHash);
|
||||
image.BlurHash = string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path);
|
||||
var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
|
||||
anyChange = anyChange || modifiedDate != image.DateModified;
|
||||
image.DateModified = modifiedDate;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -2043,20 +2052,28 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
_itemRepository.SaveImages(item);
|
||||
if (anyChange)
|
||||
{
|
||||
_itemRepository.SaveImages(item);
|
||||
}
|
||||
|
||||
RegisterItem(item);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
{
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
|
||||
|
||||
// Modify again, so saved value is after write time of externally saved metadata
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
|
||||
if (ItemUpdated is not null)
|
||||
{
|
||||
foreach (var item in items)
|
||||
@@ -2097,8 +2114,6 @@ namespace Emby.Server.Implementations.Library
|
||||
await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
|
||||
await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -2384,12 +2399,13 @@ namespace Emby.Server.Implementations.Library
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
|
||||
var lastRefreshedUtc = item.DateLastRefreshed;
|
||||
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
|
||||
|
||||
if (!refresh && !item.DisplayParentId.IsEmpty())
|
||||
{
|
||||
var displayParent = GetItemById(item.DisplayParentId);
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
|
||||
}
|
||||
|
||||
if (refresh)
|
||||
@@ -2447,12 +2463,13 @@ namespace Emby.Server.Implementations.Library
|
||||
isNew = true;
|
||||
}
|
||||
|
||||
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
|
||||
var lastRefreshedUtc = item.DateLastRefreshed;
|
||||
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
|
||||
|
||||
if (!refresh && !item.DisplayParentId.IsEmpty())
|
||||
{
|
||||
var displayParent = GetItemById(item.DisplayParentId);
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
|
||||
}
|
||||
|
||||
if (refresh)
|
||||
@@ -2522,12 +2539,13 @@ namespace Emby.Server.Implementations.Library
|
||||
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
|
||||
var lastRefreshedUtc = item.DateLastRefreshed;
|
||||
var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
|
||||
|
||||
if (!refresh && !item.DisplayParentId.IsEmpty())
|
||||
{
|
||||
var displayParent = GetItemById(item.DisplayParentId);
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
|
||||
refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
|
||||
}
|
||||
|
||||
if (refresh)
|
||||
@@ -2991,13 +3009,12 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var path = Person.GetPath(person.Name);
|
||||
var info = Directory.CreateDirectory(path);
|
||||
var lastWriteTime = info.LastWriteTimeUtc;
|
||||
personEntity = new Person()
|
||||
{
|
||||
Name = person.Name,
|
||||
Id = GetItemByNameId<Person>(path),
|
||||
DateCreated = info.CreationTimeUtc,
|
||||
DateModified = lastWriteTime,
|
||||
DateModified = info.LastWriteTimeUtc,
|
||||
Path = path
|
||||
};
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.Library
|
||||
if (fileCreationDate is not null)
|
||||
{
|
||||
var dateCreated = fileCreationDate;
|
||||
if (dateCreated.Equals(DateTime.MinValue))
|
||||
if (dateCreated == DateTime.MinValue)
|
||||
{
|
||||
dateCreated = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
|
||||
"TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
|
||||
"TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
|
||||
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة."
|
||||
"TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
|
||||
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
|
||||
"CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"NameInstallFailed": "{0} instal·lació fallida",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameSeasonUnknown": "Temporada desconeguda",
|
||||
"NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
|
||||
"NewVersionIsAvailable": "Hi ha disponible una versió nova del servidor de Jellyfin per a la descàrrega.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada",
|
||||
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
|
||||
@@ -64,7 +64,7 @@
|
||||
"Playlists": "Llistes de reproducció",
|
||||
"Plugin": "Complement",
|
||||
"PluginInstalledWithName": "{0} ha estat instal·lat",
|
||||
"PluginUninstalledWithName": "S'ha instalat {0}",
|
||||
"PluginUninstalledWithName": "S'ha instal·lat {0}",
|
||||
"PluginUpdatedWithName": "S'ha actualitzat {0}",
|
||||
"ProviderValue": "Proveïdor: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} ha fallat",
|
||||
@@ -93,50 +93,50 @@
|
||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||
"VersionNumber": "Versió {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
|
||||
"TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
|
||||
"TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin",
|
||||
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.",
|
||||
"TaskRefreshChannels": "Actualitza els canals",
|
||||
"TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
|
||||
"TaskCleanTranscode": "Neteja les transcodificacions",
|
||||
"TaskCleanTranscode": "Neteja de les transcodificacions",
|
||||
"TaskUpdatePluginsDescription": "Descarrega i instal·la els complements que estiguin configurats per a actualitzar-se automàticament.",
|
||||
"TaskUpdatePlugins": "Actualitza els complements",
|
||||
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la mediateca.",
|
||||
"TaskRefreshPeople": "Actualitza les persones",
|
||||
"TaskUpdatePlugins": "Actualització dels complements",
|
||||
"TaskRefreshPeopleDescription": "Actualització de les metadades dels actors i directors de la mediateca.",
|
||||
"TaskRefreshPeople": "Actualització de les persones",
|
||||
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
|
||||
"TaskCleanLogs": "Neteja els registres",
|
||||
"TaskRefreshLibraryDescription": "Escaneja la mediateca, a la cerca de fitxers nous i refresca les metadades.",
|
||||
"TaskRefreshLibrary": "Escaneja la mediateca",
|
||||
"TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
|
||||
"TaskRefreshChapterImages": "Extreu les imatges dels capítols",
|
||||
"TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.",
|
||||
"TaskCleanCache": "Elimina la memòria cau",
|
||||
"TaskCleanLogs": "Neteja dels registres",
|
||||
"TaskRefreshLibraryDescription": "Escaneig de la mediateca, a la cerca de fitxers nous i refresca les metadades.",
|
||||
"TaskRefreshLibrary": "Escaneig de la mediateca",
|
||||
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
|
||||
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
|
||||
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
|
||||
"TaskCleanCache": "Eliminació de la memòria cau",
|
||||
"TasksChannelsCategory": "Canals per internet",
|
||||
"TasksApplicationCategory": "Aplicatiu",
|
||||
"TasksLibraryCategory": "Mediateca",
|
||||
"TasksMaintenanceCategory": "Manteniment",
|
||||
"TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
|
||||
"TaskCleanActivityLog": "Buida el registre d'activitat",
|
||||
"TaskCleanActivityLogDescription": "Eliminació de les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
|
||||
"TaskCleanActivityLog": "Buidat del registre d'activitat",
|
||||
"Undefined": "Indefinit",
|
||||
"Forced": "Forçat",
|
||||
"Default": "Per defecte",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la mediateca o fer d'altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
|
||||
"TaskOptimizeDatabase": "Optimitza la base de dades",
|
||||
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.",
|
||||
"TaskOptimizeDatabase": "Optimització de la base de dades",
|
||||
"TaskKeyframeExtractorDescription": "Extractor de fotogrames clau dels fitxers de vídeo per a crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.",
|
||||
"TaskKeyframeExtractor": "Extractor de fotogrames clau",
|
||||
"External": "Extern",
|
||||
"HearingImpaired": "Discapacitat auditiva",
|
||||
"TaskRefreshTrickplayImages": "Genera imatges de previsualització",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea imatges de previsualització per vídeos en les mediateques habilitades.",
|
||||
"TaskRefreshTrickplayImages": "Generació d'imatges de previsualització",
|
||||
"TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció",
|
||||
"TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció",
|
||||
"TaskAudioNormalization": "Estabilització de l'àudio",
|
||||
"TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització de l'àudio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Baixa les lletres de les cançons",
|
||||
"TaskDownloadMissingLyrics": "Baixa les lletres que falten",
|
||||
"TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",
|
||||
"TaskDownloadMissingLyrics": "Descàrrega de les lletres que faltin",
|
||||
"TaskExtractMediaSegments": "Escaneig de segments multimèdia",
|
||||
"TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
|
||||
"TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la mediateca.",
|
||||
"TaskMoveTrickplayImages": "Migració de la ubicació de la imatge de previsualització",
|
||||
"TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.",
|
||||
"CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
|
||||
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Media Segment Scan",
|
||||
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
|
||||
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.",
|
||||
"CleanupUserDataTask": "User data cleanup task",
|
||||
"CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favourite status etc) from media that is no longer present for at least 90 days."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
|
||||
"TaskExtractMediaSegments": "Escaneo de segmentos de medios",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay"
|
||||
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
|
||||
"CleanupUserDataTask": "Tarea de limpieza de datos del usuario",
|
||||
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
|
||||
"TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
|
||||
"TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
|
||||
"TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu."
|
||||
"TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu.",
|
||||
"CleanupUserDataTaskDescription": "Gutxienez 90 egunez dagoeneko existitzen ez den multimediatik erabiltzaile-datu guztiak (ikusteko egoera, gogokoen egoera, etab.) garbitzen ditu.",
|
||||
"CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskDownloadMissingLyricsDescription": "Ladataan sanoituksia",
|
||||
"TaskExtractMediaSegmentsDescription": "Poimii tai hankkii mediasegmenttejä MediaSegment-yhteensopivista laajennuksista.",
|
||||
"TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti",
|
||||
"TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan."
|
||||
"TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan.",
|
||||
"CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä",
|
||||
"CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskUpdatePlugins": "Nuashonraigh Breiseáin",
|
||||
"TaskCleanTranscodeDescription": "Scriostar comhaid traschódaithe níos mó ná lá amháin d'aois.",
|
||||
"TaskCleanTranscode": "Eolaire Transcode Glan",
|
||||
"TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh"
|
||||
"TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh",
|
||||
"CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora",
|
||||
"CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,6 @@
|
||||
"TaskMoveTrickplayImages": "Migrar a localización da imaxe de Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.",
|
||||
"TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay",
|
||||
"TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio."
|
||||
"TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio.",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de datos do usuario"
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"LabelIpAddressValue": "Ip כתובת: {0}",
|
||||
"LabelRunningTimeValue": "משך צפייה: {0}",
|
||||
"Latest": "אחרון",
|
||||
"MessageApplicationUpdated": "שרת ג'ליפין עודכן",
|
||||
"MessageApplicationUpdatedTo": "שרת ג'ליפין עודכן לגרסה {0}",
|
||||
"MessageApplicationUpdated": "שרת Jellyfin עודכן",
|
||||
"MessageApplicationUpdatedTo": "שרת Jellyfin עודכן לגרסה {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
|
||||
"MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
|
||||
"MixedContent": "תוכן מעורב",
|
||||
@@ -43,7 +43,7 @@
|
||||
"NameInstallFailed": "התקנת {0} נכשלה",
|
||||
"NameSeasonNumber": "עונה {0}",
|
||||
"NameSeasonUnknown": "עונה לא ידועה",
|
||||
"NewVersionIsAvailable": "גרסה חדשה של שרת ג'ליפין זמינה להורדה.",
|
||||
"NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
|
||||
"NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
|
||||
"NotificationOptionAudioPlayback": "ניגון שמע החל",
|
||||
@@ -72,7 +72,7 @@
|
||||
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
|
||||
"Shows": "סדרות",
|
||||
"Songs": "שירים",
|
||||
"StartupEmbyServerIsLoading": "שרת ג'ליפין טוען. נא לנסות שוב בקרוב.",
|
||||
"StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
|
||||
"Sync": "סנכרון",
|
||||
@@ -100,14 +100,14 @@
|
||||
"TasksLibraryCategory": "ספרייה",
|
||||
"TasksMaintenanceCategory": "תחזוקה",
|
||||
"TaskUpdatePlugins": "עדכן תוספים",
|
||||
"TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
|
||||
"TaskRefreshPeopleDescription": "מעדכן מטא-דאטה עבור שחקנים ובמאים בספריית המדיה שלך.",
|
||||
"TaskRefreshPeople": "רענן אנשים",
|
||||
"TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
|
||||
"TaskCleanLogs": "ניקוי תיקיית יומן",
|
||||
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
|
||||
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא-דאטה.",
|
||||
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
|
||||
"TasksChannelsCategory": "ערוצי אינטרנט",
|
||||
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט כתוביות חסרות בהתבסס על המטא-דאטה.",
|
||||
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
|
||||
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
|
||||
"TaskRefreshChannels": "רענן ערוץ",
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay",
|
||||
"TaskExtractMediaSegments": "סריקת מדיה",
|
||||
"TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
|
||||
"TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה."
|
||||
"TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה.",
|
||||
"CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
|
||||
"CleanupUserDataTask": "משימת ניקוי מידע משתמש"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
|
||||
"TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése",
|
||||
"TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.",
|
||||
"TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből."
|
||||
"TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből.",
|
||||
"CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.",
|
||||
"CleanupUserDataTask": "Felhasználói adatok tisztítása feladat"
|
||||
}
|
||||
|
||||
@@ -129,5 +129,13 @@
|
||||
"TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
|
||||
"TaskAudioNormalization": "Normalisasi Audio",
|
||||
"TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada.",
|
||||
"TaskDownloadMissingLyricsDescription": "Unduh lirik untuk lagu",
|
||||
"TaskExtractMediaSegmentsDescription": "Mengekstrak atau memperoleh segmen media dari plugin yang mendukung MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Memindahkan file trickplay yang sudah ada sesuai dengan pengaturan pustaka.",
|
||||
"CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (status tontonan, status favorit, dll.) dari media yang sudah tidak ada selama setidaknya 90 hari.",
|
||||
"TaskExtractMediaSegments": "Scan Segmen media",
|
||||
"TaskMoveTrickplayImages": "Migrasikan Lokasi Gambar Trickplay",
|
||||
"TaskDownloadMissingLyrics": "Unduh Lirik yang Hilang",
|
||||
"CleanupUserDataTask": "Tugas Pembersihan Data Pengguna"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "Sposta le immagini Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria.",
|
||||
"TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.",
|
||||
"TaskExtractMediaSegments": "Scansiona Segmento Media"
|
||||
"TaskExtractMediaSegments": "Scansiona Segmento Media",
|
||||
"CleanupUserDataTask": "Task di pulizia dei dati utente",
|
||||
"CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskMoveTrickplayImages": "Trickplayの画像を移動",
|
||||
"TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。",
|
||||
"TaskDownloadMissingLyrics": "失われた歌詞をダウンロード",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。"
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。",
|
||||
"CleanupUserDataTask": "ユーザーデータのクリーンアップタスク",
|
||||
"CleanupUserDataTaskDescription": "90日以上存在しないメディアに対して、視聴状態やお気に入り状態などのユーザーデータをすべて削除します。"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
|
||||
"DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
|
||||
"External": "ಹೊರಗಿನ",
|
||||
"FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
|
||||
"FailedLoginAttemptWithUserName": "ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ ಸಂಖ್ಯೆ {0}",
|
||||
"Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
|
||||
"Folders": "ಫೋಲ್ಡರ್ಗಳು",
|
||||
"Forced": "ಬಲವಂತವಾಗಿ",
|
||||
@@ -123,5 +123,13 @@
|
||||
"TaskUpdatePlugins": "ಪ್ಲಗಿನ್ಗಳನ್ನು ನವೀಕರಿಸಿ",
|
||||
"TaskCleanTranscode": "ಟ್ರಾನ್ಸ್ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
|
||||
"TaskRefreshChannels": "ಚಾನಲ್ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
|
||||
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
|
||||
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
|
||||
"TaskAudioNormalizationDescription": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ ಮಾಹಿತಿಗಾಗಿ ಕಡತಗಳನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ.",
|
||||
"TaskDownloadMissingLyricsDescription": "ಹಾಡುಗಳಿಗೆ ಸಾಹಿತ್ಯ ಪಡೆಯಿರಿ",
|
||||
"TaskExtractMediaSegments": "ಮಾಧ್ಯಮ ವಿಭಾಗದ ಹುಡುಕು",
|
||||
"TaskDownloadMissingLyrics": "ಇಲ್ಲದ ಸಾಹಿತ್ಯವನ್ನು ಪಡೆಯಿರಿ",
|
||||
"TaskAudioNormalization": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ",
|
||||
"TaskRefreshTrickplayImages": "ಟ್ರಿಕ್ಪ್ಲೇ ಚಿತ್ರಗಳನ್ನು ರಚಿಸಿ",
|
||||
"TaskCleanCollectionsAndPlaylists": "ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "ಇಲ್ಲದ ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳಿಂದ ವಸ್ತುಗಳನ್ನು ತೆಗೆದುಹಾಕುತ್ತದೆ."
|
||||
}
|
||||
|
||||
@@ -130,5 +130,7 @@
|
||||
"TaskExtractMediaSegments": "मिडिया विभाग तपासणी",
|
||||
"TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा",
|
||||
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
|
||||
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण"
|
||||
"TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
|
||||
"TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
|
||||
"TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan senarai audio video",
|
||||
"TaskAudioNormalization": "Normalisasi Audio",
|
||||
"TaskAudioNormalizationDescription": "Mengimbas fail-fail untuk data normalisasi audio.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Mengalih keluar item daripada koleksi dan senarai audio video yang tidak wujud lagi."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Mengalih keluar item daripada koleksi dan senarai audio video yang tidak wujud lagi.",
|
||||
"CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (keadaan tontonan, status kegemaran, dan sebagainya) daripada media yang tidak lagi wujud sekurang-kurangnya selama 90 hari.",
|
||||
"CleanupUserDataTask": "Tugas pembersihan data pengguna"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Move os arquivos do trickplay de acordo com as configurações da biblioteca.",
|
||||
"TaskExtractMediaSegments": "Varredura do segmento de mídia",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de mídia de plug-ins habilitados para MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay"
|
||||
"TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay",
|
||||
"CleanupUserDataTask": "Tarefa de limpeza de dados do usuário",
|
||||
"CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay",
|
||||
"TaskDownloadMissingLyricsDescription": "Transferir letra para músicas",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de multimédia a partir de plugins com suporte para MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca."
|
||||
"TaskMoveTrickplayImagesDescription": "Move os ficheiros trickplay existentes de acordo com as definições da mediateca.",
|
||||
"CleanupUserDataTaskDescription": "Apaga todos os dados de utilizador (estados de reprodução, favoritos, etc) de arquivos média não presentes há 90 dias ou mais.",
|
||||
"CleanupUserDataTask": "Limpeza de dados de utilizador"
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"TaskCleanTranscodeDescription": "Șterge fișierele de transcodare mai vechi de o zi.",
|
||||
"TaskCleanTranscode": "Curățați directorul de transcodare",
|
||||
"TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru extensiile care sunt configurate să se actualizeze automat.",
|
||||
"TaskUpdatePlugins": "Actualizați Extensile",
|
||||
"TaskUpdatePlugins": "Actualizați Extensiile",
|
||||
"TaskRefreshPeopleDescription": "Actualizează metadatele pentru actori și regizori din biblioteca media.",
|
||||
"TaskRefreshPeople": "Actualizează Persoanele",
|
||||
"TaskCleanLogsDescription": "Șterge fișierele jurnal care au mai mult de {0} zile.",
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Extrage sau obține segmentele media de la pluginurile MediaSegment activate.",
|
||||
"TaskMoveTrickplayImages": "Migrează locația imaginii Trickplay",
|
||||
"TaskDownloadMissingLyrics": "Descarcă versurile lipsă",
|
||||
"TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii"
|
||||
"TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii",
|
||||
"CleanupUserDataTask": "Sarcina de curatare a datelor utilizatorului",
|
||||
"CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile."
|
||||
}
|
||||
|
||||
@@ -138,5 +138,5 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Извлекает или получает медиасегменты из плагинов MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки.",
|
||||
"CleanupUserDataTask": "Задача очистки пользовательских данных",
|
||||
"CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с носителей, на которых больше нет информации, по крайней мере, в течение 90 дней."
|
||||
"CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "Presunúť umiestnenie obrázkov Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Presunie existujúce súbory Trickplay podľa nastavení knižnice.",
|
||||
"TaskDownloadMissingLyrics": "Stiahnuť chýbajúce texty piesní",
|
||||
"TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne"
|
||||
"TaskDownloadMissingLyricsDescription": "Stiahne texty pre piesne",
|
||||
"CleanupUserDataTask": "Prečistiť používateľské dáta",
|
||||
"CleanupUserDataTaskDescription": "Vyčistí všetky dáta používateľa (stav sledovania, stav obľúbených atď.) z médií, ktoré už neexistujú aspoň 90 dní."
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"Inherit": "மரபுரிமையாகப் பெறு",
|
||||
"HeaderRecordingGroups": "பதிவு குழுக்கள்",
|
||||
"Folders": "கோப்புறைகள்",
|
||||
"FailedLoginAttemptWithUserName": "{0} இன் உள்நுழைவு முயற்சி தோல்வியடைந்தது",
|
||||
"FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
|
||||
"DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
|
||||
"DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
|
||||
"Collections": "தொகுப்புகள்",
|
||||
@@ -133,5 +133,9 @@
|
||||
"TaskDownloadMissingLyrics": "விடுபட்ட பாடல் வரிகளைப் பதிவிறக்கவும்",
|
||||
"TaskDownloadMissingLyricsDescription": "பாடல்களுக்கான வரிகளைப் பதிவிறக்குகிறது",
|
||||
"TaskMoveTrickplayImages": "ட்ரிக்பிளே பட இருப்பிடத்தை நகர்த்து",
|
||||
"TaskMoveTrickplayImagesDescription": "நூலக அமைப்புகளுக்கு ஏற்ப ஏற்கனவே உள்ள ட்ரிக்பிளே கோப்புகளை நகர்த்துகிறது."
|
||||
"TaskMoveTrickplayImagesDescription": "நூலக அமைப்புகளுக்கு ஏற்ப ஏற்கனவே உள்ள ட்ரிக்பிளே கோப்புகளை நகர்த்துகிறது.",
|
||||
"TaskExtractMediaSegments": "மீடியா பிரிவு ஸ்கேன்",
|
||||
"TaskExtractMediaSegmentsDescription": "மீடியாசெக்மென்ட் இயக்கப்பட்ட செருகுநிரல்களிலிருந்து மீடியா பிரிவுகளைப் பிரித்தெடுக்கிறது அல்லது பெறுகிறது.",
|
||||
"CleanupUserDataTaskDescription": "குறைந்தது 90 நாட்களுக்கு இல்லாத மீடியாவிலிருந்து அனைத்து பயனர் தரவையும் (கண்காணிப்பு நிலை, பிடித்த நிலை போன்றவை) சுத்தம் செய்கிறது.",
|
||||
"CleanupUserDataTask": "பயனர் தரவை சுத்தம் செய்யும் பணி"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Mevcut trickplay dosyalarını kütüphane ayarlarına göre taşır.",
|
||||
"TaskDownloadMissingLyrics": "Eksik şarkı sözlerini indir",
|
||||
"TaskDownloadMissingLyricsDescription": "Şarkı sözlerini indirir",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır."
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır.",
|
||||
"CleanupUserDataTask": "Kullanıcı verisi temizleme görevi",
|
||||
"CleanupUserDataTaskDescription": "En az 90 gün boyunca artık mevcut olmayan medyadaki tüm kullanıcı verilerini (İzleme durumu, favori durumu vb.) temizler."
|
||||
}
|
||||
|
||||
@@ -423,7 +423,7 @@ namespace Emby.Server.Implementations.Plugins
|
||||
Overview = packageInfo.Overview,
|
||||
Owner = packageInfo.Owner,
|
||||
TargetAbi = versionInfo.TargetAbi ?? string.Empty,
|
||||
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture),
|
||||
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal),
|
||||
Version = versionInfo.Version,
|
||||
Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
|
||||
AutoUpdate = true,
|
||||
|
||||
@@ -474,6 +474,7 @@ namespace Emby.Server.Implementations.Session
|
||||
private void RemoveNowPlayingItem(SessionInfo session)
|
||||
{
|
||||
session.NowPlayingItem = null;
|
||||
session.FullNowPlayingItem = null;
|
||||
session.PlayState = new PlayerStateInfo();
|
||||
|
||||
if (!string.IsNullOrEmpty(session.DeviceId))
|
||||
|
||||
@@ -5,7 +5,6 @@ using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Sorting;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace Emby.Server.Implementations.Sorting
|
||||
{
|
||||
|
||||
@@ -256,7 +256,7 @@ public sealed class BaseItemRepository
|
||||
dbQuery = ApplyGroupingFilter(dbQuery, filter);
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
|
||||
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
result.StartIndex = filter.StartIndex ?? 0;
|
||||
return result;
|
||||
}
|
||||
@@ -275,7 +275,7 @@ public sealed class BaseItemRepository
|
||||
dbQuery = ApplyGroupingFilter(dbQuery, filter);
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
|
||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -317,7 +317,7 @@ public sealed class BaseItemRepository
|
||||
mainquery = ApplyGroupingFilter(mainquery, filter);
|
||||
mainquery = ApplyQueryPaging(mainquery, filter);
|
||||
|
||||
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -540,7 +540,7 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
var itemValueMaps = tuples
|
||||
.Select(e => (Item: e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
|
||||
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
|
||||
.ToArray();
|
||||
var allListedItemValues = itemValueMaps
|
||||
.SelectMany(f => f.Values)
|
||||
@@ -567,7 +567,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
|
||||
var valueMap = itemValueMaps
|
||||
.Select(f => (Item: f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).ToArray()))
|
||||
.Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).DistinctBy(e => e.ItemValueId).ToArray()))
|
||||
.ToArray();
|
||||
|
||||
var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
|
||||
@@ -655,7 +655,7 @@ public sealed class BaseItemRepository
|
||||
return null;
|
||||
}
|
||||
|
||||
return DeserialiseBaseItem(item);
|
||||
return DeserializeBaseItem(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -701,12 +701,12 @@ public sealed class BaseItemRepository
|
||||
dto.TotalBitrate = entity.TotalBitrate;
|
||||
dto.ExternalId = entity.ExternalId;
|
||||
dto.Size = entity.Size;
|
||||
dto.Genres = entity.Genres?.Split('|') ?? [];
|
||||
dto.DateCreated = entity.DateCreated.GetValueOrDefault();
|
||||
dto.DateModified = entity.DateModified.GetValueOrDefault();
|
||||
dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
|
||||
dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
|
||||
dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
|
||||
dto.ChannelId = entity.ChannelId ?? Guid.Empty;
|
||||
dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault();
|
||||
dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault();
|
||||
dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
|
||||
dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
|
||||
dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
|
||||
dto.Width = entity.Width.GetValueOrDefault();
|
||||
dto.Height = entity.Height.GetValueOrDefault();
|
||||
@@ -733,7 +733,7 @@ public sealed class BaseItemRepository
|
||||
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
|
||||
dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
|
||||
dto.Studios = entity.Studios?.Split('|') ?? [];
|
||||
dto.Tags = entity.Tags?.Split('|') ?? [];
|
||||
dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
|
||||
|
||||
if (dto is IHasProgramAttributes hasProgramAttributes)
|
||||
{
|
||||
@@ -807,7 +807,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (dto is Folder folder)
|
||||
{
|
||||
folder.DateLastMediaAdded = entity.DateLastMediaAdded;
|
||||
folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
return dto;
|
||||
@@ -867,11 +867,11 @@ public sealed class BaseItemRepository
|
||||
entity.ExternalId = dto.ExternalId;
|
||||
entity.Size = dto.Size;
|
||||
entity.Genres = string.Join('|', dto.Genres);
|
||||
entity.DateCreated = dto.DateCreated;
|
||||
entity.DateModified = dto.DateModified;
|
||||
entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
|
||||
entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
|
||||
entity.ChannelId = dto.ChannelId;
|
||||
entity.DateLastRefreshed = dto.DateLastRefreshed;
|
||||
entity.DateLastSaved = dto.DateLastSaved;
|
||||
entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
|
||||
entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
|
||||
entity.OwnerId = dto.OwnerId.ToString();
|
||||
entity.Width = dto.Width;
|
||||
entity.Height = dto.Height;
|
||||
@@ -981,7 +981,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (dto is Folder folder)
|
||||
{
|
||||
entity.DateLastMediaAdded = folder.DateLastMediaAdded;
|
||||
entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdded;
|
||||
entity.IsFolder = folder.IsFolder;
|
||||
}
|
||||
|
||||
@@ -1017,7 +1017,7 @@ public sealed class BaseItemRepository
|
||||
return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
|
||||
}
|
||||
|
||||
private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
|
||||
private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
|
||||
if (_serverConfigurationManager?.Configuration is null)
|
||||
@@ -1026,7 +1026,7 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
var typeToSerialise = GetType(baseItemEntity.Type);
|
||||
return BaseItemRepository.DeserialiseBaseItem(
|
||||
return BaseItemRepository.DeserializeBaseItem(
|
||||
baseItemEntity,
|
||||
_logger,
|
||||
_appHost,
|
||||
@@ -1034,7 +1034,7 @@ public sealed class BaseItemRepository
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialises a BaseItemEntity and sets all properties.
|
||||
/// Deserializes a BaseItemEntity and sets all properties.
|
||||
/// </summary>
|
||||
/// <param name="baseItemEntity">The DB entity.</param>
|
||||
/// <param name="logger">Logger.</param>
|
||||
@@ -1042,9 +1042,9 @@ public sealed class BaseItemRepository
|
||||
/// <param name="skipDeserialization">If only mapping should be processed.</param>
|
||||
/// <returns>A mapped BaseItem.</returns>
|
||||
/// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
|
||||
public static BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
|
||||
public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
|
||||
{
|
||||
var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unknown type.");
|
||||
var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
|
||||
BaseItemDto? dto = null;
|
||||
if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
|
||||
{
|
||||
@@ -1060,7 +1060,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (dto is null)
|
||||
{
|
||||
dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unknown type.");
|
||||
dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
|
||||
}
|
||||
|
||||
return Map(baseItemEntity, dto, appHost);
|
||||
@@ -1206,7 +1206,7 @@ public sealed class BaseItemRepository
|
||||
.Where(e => e is not null)
|
||||
.Select(e =>
|
||||
{
|
||||
return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
|
||||
return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1221,7 +1221,7 @@ public sealed class BaseItemRepository
|
||||
.Where(e => e is not null)
|
||||
.Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
|
||||
{
|
||||
return (DeserialiseBaseItem(e, filter.SkipDeserialization), null);
|
||||
return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1302,7 +1302,7 @@ public sealed class BaseItemRepository
|
||||
{
|
||||
Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
|
||||
BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
|
||||
DateModified = e.DateModified,
|
||||
DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
|
||||
Height = e.Height,
|
||||
Width = e.Width,
|
||||
Type = (ImageType)e.ImageType
|
||||
|
||||
@@ -253,7 +253,7 @@ namespace Jellyfin.Server.Extensions
|
||||
c.AddSwaggerTypeMappings();
|
||||
|
||||
c.SchemaFilter<IgnoreEnumSchemaFilter>();
|
||||
c.OperationFilter<RetryOnTemporarlyUnavailableFilter>();
|
||||
c.OperationFilter<RetryOnTemporarilyUnavailableFilter>();
|
||||
c.OperationFilter<SecurityRequirementsOperationFilter>();
|
||||
c.OperationFilter<FileResponseFilter>();
|
||||
c.OperationFilter<FileRequestFilter>();
|
||||
|
||||
@@ -6,13 +6,13 @@ using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Jellyfin.Server.Filters;
|
||||
|
||||
internal class RetryOnTemporarlyUnavailableFilter : IOperationFilter
|
||||
internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Responses.Add("503", new OpenApiResponse()
|
||||
{
|
||||
Description = "The server is currently starting or is temporarly not available.",
|
||||
Description = "The server is currently starting or is temporarily not available.",
|
||||
Headers = new Dictionary<string, OpenApiHeader>()
|
||||
{
|
||||
{
|
||||
@@ -53,6 +53,7 @@
|
||||
<PackageReference Include="prometheus-net.AspNetCore" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" />
|
||||
<PackageReference Include="Serilog.Expressions" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
|
||||
168
Jellyfin.Server/Migrations/Routines/FixDates.cs
Normal file
168
Jellyfin.Server/Migrations/Routines/FixDates.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
/// <summary>
|
||||
/// Migration to fix dates saved in the database to always be UTC.
|
||||
/// </summary>
|
||||
[JellyfinMigration("2025-06-20T18:00:00", nameof(FixDates))]
|
||||
public class FixDates : IAsyncMigrationRoutine
|
||||
{
|
||||
private const int PageSize = 5000;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FixDates"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="startupLogger">The startup logger for Startup UI integration.</param>
|
||||
/// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
|
||||
public FixDates(
|
||||
ILogger<FixDates> logger,
|
||||
IStartupLogger<FixDates> startupLogger,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider)
|
||||
{
|
||||
_logger = startupLogger.With(logger);
|
||||
_dbProvider = dbProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TimeZoneInfo.Local.Equals(TimeZoneInfo.Utc))
|
||||
{
|
||||
using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
await FixBaseItemsAsync(context, sw, cancellationToken).ConfigureAwait(false);
|
||||
sw.Reset();
|
||||
await FixChaptersAsync(context, sw, cancellationToken).ConfigureAwait(false);
|
||||
sw.Reset();
|
||||
await FixBaseItemImageInfos(context, sw, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FixBaseItemsAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
|
||||
{
|
||||
int itemCount = 0;
|
||||
|
||||
var baseQuery = context.BaseItems.OrderBy(e => e.Id);
|
||||
var records = baseQuery.Count();
|
||||
_logger.LogInformation("Fixing dates for {Count} BaseItems.", records);
|
||||
|
||||
sw.Start();
|
||||
await foreach (var result in context.BaseItems.OrderBy(e => e.Id)
|
||||
.WithPartitionProgress(
|
||||
(partition) =>
|
||||
_logger.LogInformation(
|
||||
"Processing BaseItems batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
|
||||
partition + 1,
|
||||
Math.Min((partition + 1) * PageSize, records),
|
||||
records,
|
||||
sw.Elapsed))
|
||||
.PartitionEagerAsync(PageSize, cancellationToken)
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
result.DateCreated = ToUniversalTime(result.DateCreated);
|
||||
result.DateLastMediaAdded = ToUniversalTime(result.DateLastMediaAdded);
|
||||
result.DateLastRefreshed = ToUniversalTime(result.DateLastRefreshed);
|
||||
result.DateLastSaved = ToUniversalTime(result.DateLastSaved);
|
||||
result.DateModified = ToUniversalTime(result.DateModified);
|
||||
itemCount++;
|
||||
}
|
||||
|
||||
var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("BaseItems: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
|
||||
}
|
||||
|
||||
private async Task FixChaptersAsync(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
|
||||
{
|
||||
int itemCount = 0;
|
||||
|
||||
var baseQuery = context.Chapters;
|
||||
var records = baseQuery.Count();
|
||||
_logger.LogInformation("Fixing dates for {Count} Chapters.", records);
|
||||
|
||||
sw.Start();
|
||||
await foreach (var result in context.Chapters.OrderBy(e => e.ItemId)
|
||||
.WithPartitionProgress(
|
||||
(partition) =>
|
||||
_logger.LogInformation(
|
||||
"Processing Chapter batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
|
||||
partition + 1,
|
||||
Math.Min((partition + 1) * PageSize, records),
|
||||
records,
|
||||
sw.Elapsed))
|
||||
.PartitionEagerAsync(PageSize, cancellationToken)
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
result.ImageDateModified = ToUniversalTime(result.ImageDateModified, true);
|
||||
itemCount++;
|
||||
}
|
||||
|
||||
var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("Chapters: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
|
||||
}
|
||||
|
||||
private async Task FixBaseItemImageInfos(JellyfinDbContext context, Stopwatch sw, CancellationToken cancellationToken)
|
||||
{
|
||||
int itemCount = 0;
|
||||
|
||||
var baseQuery = context.BaseItemImageInfos;
|
||||
var records = baseQuery.Count();
|
||||
_logger.LogInformation("Fixing dates for {Count} BaseItemImageInfos.", records);
|
||||
|
||||
sw.Start();
|
||||
await foreach (var result in context.BaseItemImageInfos.OrderBy(e => e.Id)
|
||||
.WithPartitionProgress(
|
||||
(partition) =>
|
||||
_logger.LogInformation(
|
||||
"Processing BaseItemImageInfos batch {BatchNumber} ({ProcessedSoFar}/{TotalRecords}) - Time: {ElapsedTime}",
|
||||
partition + 1,
|
||||
Math.Min((partition + 1) * PageSize, records),
|
||||
records,
|
||||
sw.Elapsed))
|
||||
.PartitionEagerAsync(PageSize, cancellationToken)
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
result.DateModified = ToUniversalTime(result.DateModified);
|
||||
itemCount++;
|
||||
}
|
||||
|
||||
var saveCount = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("BaseItemImageInfos: Processed {ItemCount} items, saved {SaveCount} changes in {ElapsedTime}", itemCount, saveCount, sw.Elapsed);
|
||||
}
|
||||
|
||||
private DateTime? ToUniversalTime(DateTime? dateTime, bool isUTC = false)
|
||||
{
|
||||
if (dateTime is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dateTime.Value.Year == 1 && dateTime.Value.Month == 1 && dateTime.Value.Day == 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dateTime.Value.Kind == DateTimeKind.Utc || isUTC)
|
||||
{
|
||||
return dateTime.Value;
|
||||
}
|
||||
|
||||
return dateTime.Value.ToUniversalTime();
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
connection.Open();
|
||||
|
||||
var baseItemIds = new HashSet<Guid>();
|
||||
using (var operation = GetPreparedDbContext("moving TypedBaseItem"))
|
||||
using (var operation = GetPreparedDbContext("Moving TypedBaseItem"))
|
||||
{
|
||||
const string typedBaseItemsQuery =
|
||||
"""
|
||||
@@ -121,13 +121,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving ItemValues"))
|
||||
using (var operation = GetPreparedDbContext("Moving ItemValues"))
|
||||
{
|
||||
// do not migrate inherited types as they are now properly mapped in search and lookup.
|
||||
const string itemValueQuery =
|
||||
@@ -138,7 +138,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
|
||||
// EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
|
||||
var localItems = new Dictionary<(int Type, string Value), (Database.Implementations.Entities.ItemValue ItemValue, List<Guid> ItemIds)>();
|
||||
using (new TrackedMigrationStep("loading ItemValues", _logger))
|
||||
using (new TrackedMigrationStep("Loading ItemValues", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
|
||||
{
|
||||
@@ -166,13 +166,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving UserData"))
|
||||
using (var operation = GetPreparedDbContext("Moving UserData"))
|
||||
{
|
||||
var queryResult = connection.Query(
|
||||
"""
|
||||
@@ -181,14 +181,14 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
|
||||
""");
|
||||
|
||||
using (new TrackedMigrationStep("loading UserData", _logger))
|
||||
using (new TrackedMigrationStep("Loading UserData", _logger))
|
||||
{
|
||||
var users = operation.JellyfinDbContext.Users.AsNoTracking().ToImmutableArray();
|
||||
var users = operation.JellyfinDbContext.Users.AsNoTracking().ToArray();
|
||||
var userIdBlacklist = new HashSet<int>();
|
||||
|
||||
foreach (var entity in queryResult)
|
||||
{
|
||||
var userData = GetUserData(users, entity, userIdBlacklist);
|
||||
var userData = GetUserData(users, entity, userIdBlacklist, _logger);
|
||||
if (userData is null)
|
||||
{
|
||||
var userDataId = entity.GetString(0);
|
||||
@@ -212,19 +212,17 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
userData.ItemId = refItem.Id;
|
||||
operation.JellyfinDbContext.UserData.Add(userData);
|
||||
}
|
||||
|
||||
users.Clear();
|
||||
}
|
||||
|
||||
legacyBaseItemWithUserKeys.Clear();
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving MediaStreamInfos"))
|
||||
using (var operation = GetPreparedDbContext("Moving MediaStreamInfos"))
|
||||
{
|
||||
const string mediaStreamQuery =
|
||||
"""
|
||||
@@ -237,7 +235,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
|
||||
""";
|
||||
|
||||
using (new TrackedMigrationStep("loading MediaStreamInfos", _logger))
|
||||
using (new TrackedMigrationStep("Loading MediaStreamInfos", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
|
||||
{
|
||||
@@ -245,13 +243,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving AttachmentStreamInfos"))
|
||||
using (var operation = GetPreparedDbContext("Moving AttachmentStreamInfos"))
|
||||
{
|
||||
const string mediaAttachmentQuery =
|
||||
"""
|
||||
@@ -260,7 +258,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = mediaattachments.ItemId)
|
||||
""";
|
||||
|
||||
using (new TrackedMigrationStep("loading AttachmentStreamInfos", _logger))
|
||||
using (new TrackedMigrationStep("Loading AttachmentStreamInfos", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery))
|
||||
{
|
||||
@@ -268,13 +266,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} AttachmentStreamInfos entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} AttachmentStreamInfos entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving People"))
|
||||
using (var operation = GetPreparedDbContext("Moving People"))
|
||||
{
|
||||
const string personsQuery =
|
||||
"""
|
||||
@@ -284,14 +282,14 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
|
||||
var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
|
||||
|
||||
using (new TrackedMigrationStep("loading People", _logger))
|
||||
using (new TrackedMigrationStep("Loading People", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader reader in connection.Query(personsQuery))
|
||||
{
|
||||
var itemId = reader.GetGuid(0);
|
||||
if (!baseItemIds.Contains(itemId))
|
||||
{
|
||||
_logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
|
||||
_logger.LogError("Not saving person {0} because it's not in use by any BaseItem", reader.GetString(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -330,13 +328,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
peopleCache.Clear();
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving Chapters"))
|
||||
using (var operation = GetPreparedDbContext("Moving Chapters"))
|
||||
{
|
||||
const string chapterQuery =
|
||||
"""
|
||||
@@ -344,7 +342,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
|
||||
""";
|
||||
|
||||
using (new TrackedMigrationStep("loading Chapters", _logger))
|
||||
using (new TrackedMigrationStep("Loading Chapters", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(chapterQuery))
|
||||
{
|
||||
@@ -353,13 +351,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
using (var operation = GetPreparedDbContext("moving AncestorIds"))
|
||||
using (var operation = GetPreparedDbContext("Moving AncestorIds"))
|
||||
{
|
||||
const string ancestorIdsQuery =
|
||||
"""
|
||||
@@ -370,7 +368,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
|
||||
""";
|
||||
|
||||
using (new TrackedMigrationStep("loading AncestorIds", _logger))
|
||||
using (new TrackedMigrationStep("Loading AncestorIds", _logger))
|
||||
{
|
||||
foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
|
||||
{
|
||||
@@ -379,7 +377,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
}
|
||||
|
||||
using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger))
|
||||
using (new TrackedMigrationStep($"Saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger))
|
||||
{
|
||||
operation.JellyfinDbContext.SaveChanges();
|
||||
}
|
||||
@@ -404,19 +402,20 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
return new DatabaseMigrationStep(dbContext, operationName, _logger);
|
||||
}
|
||||
|
||||
private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto, HashSet<int> userIdBlacklist)
|
||||
internal static UserData? GetUserData(User[] users, SqliteDataReader dto, HashSet<int> userIdBlacklist, ILogger logger)
|
||||
{
|
||||
var internalUserId = dto.GetInt32(1);
|
||||
var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
|
||||
if (userIdBlacklist.Contains(internalUserId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
|
||||
if (user is null)
|
||||
{
|
||||
if (userIdBlacklist.Contains(internalUserId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
userIdBlacklist.Add(internalUserId);
|
||||
|
||||
_logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
|
||||
logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1168,7 +1167,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
entity.UnratedType = unratedType;
|
||||
}
|
||||
|
||||
var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
|
||||
var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
|
||||
var dataKeys = baseItem.GetUserDataKeys();
|
||||
userDataKeys.AddRange(dataKeys);
|
||||
|
||||
|
||||
107
Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
Normal file
107
Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Data;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.Implementations.Item;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using MediaBrowser.Controller;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Routines;
|
||||
|
||||
[JellyfinMigration("2025-06-18T01:00:00", nameof(MigrateLibraryUserData))]
|
||||
[JellyfinMigrationBackup(JellyfinDb = true)]
|
||||
internal class MigrateLibraryUserData : IAsyncMigrationRoutine
|
||||
{
|
||||
private const string DbFilename = "library.db.old";
|
||||
|
||||
private readonly IStartupLogger _logger;
|
||||
private readonly IServerApplicationPaths _paths;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _provider;
|
||||
|
||||
public MigrateLibraryUserData(
|
||||
IStartupLogger<MigrateLibraryDb> startupLogger,
|
||||
IDbContextFactory<JellyfinDbContext> provider,
|
||||
IServerApplicationPaths paths)
|
||||
{
|
||||
_logger = startupLogger;
|
||||
_provider = provider;
|
||||
_paths = paths;
|
||||
}
|
||||
|
||||
public async Task PerformAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Migrating the userdata from library.db.old may take a while, do not stop Jellyfin.");
|
||||
|
||||
var dataPath = _paths.DataPath;
|
||||
var libraryDbPath = Path.Combine(dataPath, DbFilename);
|
||||
if (!File.Exists(libraryDbPath))
|
||||
{
|
||||
_logger.LogError("Cannot migrate userdata from {LibraryDb} as it does not exist. This migration expects the MigrateLibraryDb to run first.", libraryDbPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
if (!await dbContext.BaseItems.AnyAsync(e => e.Id == BaseItemRepository.PlaceholderId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
// the placeholder baseitem has been deleted by the librarydb migration so we need to readd it.
|
||||
await dbContext.BaseItems.AddAsync(
|
||||
new Database.Implementations.Entities.BaseItemEntity()
|
||||
{
|
||||
Id = BaseItemRepository.PlaceholderId,
|
||||
Type = "PLACEHOLDER",
|
||||
Name = "This is a placeholder item for UserData that has been detacted from its original item"
|
||||
},
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var users = dbContext.Users.AsNoTracking().ToArray();
|
||||
var userIdBlacklist = new HashSet<int>();
|
||||
using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
|
||||
var retentionDate = DateTime.UtcNow;
|
||||
|
||||
var queryResult = connection.Query(
|
||||
"""
|
||||
SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas
|
||||
|
||||
WHERE NOT EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
|
||||
""");
|
||||
foreach (var entity in queryResult)
|
||||
{
|
||||
var userData = MigrateLibraryDb.GetUserData(users, entity, userIdBlacklist, _logger);
|
||||
if (userData is null)
|
||||
{
|
||||
var userDataId = entity.GetString(0);
|
||||
var internalUserId = entity.GetInt32(1);
|
||||
|
||||
if (!userIdBlacklist.Contains(internalUserId))
|
||||
{
|
||||
_logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId} does not match any existing user.", userDataId, internalUserId);
|
||||
userIdBlacklist.Add(internalUserId);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
userData.ItemId = BaseItemRepository.PlaceholderId;
|
||||
userData.RetentionDate = retentionDate;
|
||||
dbContext.UserData.Add(userData);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Try saving {NewSaved} UserData entries.", dbContext.UserData.Local.Count);
|
||||
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta
|
||||
}
|
||||
}
|
||||
|
||||
private class NestedStartupLogger<TCategory> : StartupLogger<TCategory>, IStartupLogger<TCategory>
|
||||
private class NestedStartupLogger<TCategory> : StartupLogger<TCategory>
|
||||
{
|
||||
public NestedStartupLogger(ILogger logger, StartupLogTopic topic) : base(logger, topic)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,6 @@ using MediaBrowser.Model.System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
@@ -29,6 +28,8 @@ using Microsoft.Extensions.Primitives;
|
||||
using Morestachio;
|
||||
using Morestachio.Framework.IO.SingleStream;
|
||||
using Morestachio.Rendering;
|
||||
using Serilog;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
@@ -143,8 +144,10 @@ public sealed class SetupServer : IDisposable
|
||||
var config = _configurationManager.GetNetworkConfiguration()!;
|
||||
_startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"])
|
||||
.UseConsoleLifetime()
|
||||
.UseSerilog()
|
||||
.ConfigureServices(serv =>
|
||||
{
|
||||
serv.AddSingleton(this);
|
||||
serv.AddHealthChecks()
|
||||
.AddCheck<SetupHealthcheck>("StartupCheck");
|
||||
serv.Configure<ForwardedHeadersOptions>(options =>
|
||||
|
||||
@@ -1423,23 +1423,16 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public virtual bool RequiresRefresh()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Path) || DateModified == default)
|
||||
if (string.IsNullOrEmpty(Path) || DateModified == DateTime.MinValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var info = FileSystem.GetFileSystemInfo(Path);
|
||||
if (info.Exists)
|
||||
{
|
||||
if (info.IsDirectory)
|
||||
{
|
||||
return info.LastWriteTimeUtc != DateModified;
|
||||
}
|
||||
|
||||
return info.LastWriteTimeUtc != DateModified;
|
||||
}
|
||||
|
||||
return false;
|
||||
return info.Exists
|
||||
? info.LastWriteTimeUtc != DateModified
|
||||
: false;
|
||||
}
|
||||
|
||||
public virtual List<string> GetUserDataKeys()
|
||||
|
||||
@@ -695,7 +695,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = GetRecursiveChildren(user, query);
|
||||
}
|
||||
|
||||
return PostFilterAndSort(items, query, true);
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
if (this is not UserRootFolder
|
||||
@@ -959,10 +959,10 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = GetChildren(user, true, childQuery).Where(filter);
|
||||
}
|
||||
|
||||
return PostFilterAndSort(items, query, true);
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query, bool enableSorting)
|
||||
protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query)
|
||||
{
|
||||
var user = query.User;
|
||||
|
||||
@@ -995,7 +995,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
|
||||
}
|
||||
|
||||
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);
|
||||
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager);
|
||||
}
|
||||
|
||||
private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded(
|
||||
|
||||
@@ -179,7 +179,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
|
||||
var items = GetEpisodes(user, query.DtoOptions, true).Where(filter);
|
||||
|
||||
return PostFilterAndSort(items, query, false);
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
PresetViews = query.PresetViews
|
||||
});
|
||||
|
||||
return UserViewBuilder.SortAndPage(result, null, query, LibraryManager, true);
|
||||
return UserViewBuilder.SortAndPage(result, null, query, LibraryManager);
|
||||
}
|
||||
|
||||
public override int GetChildCount(User user)
|
||||
|
||||
@@ -438,22 +438,18 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
|
||||
}
|
||||
|
||||
return SortAndPage(items, totalRecordLimit, query, libraryManager, true);
|
||||
return SortAndPage(items, totalRecordLimit, query, libraryManager);
|
||||
}
|
||||
|
||||
public static QueryResult<BaseItem> SortAndPage(
|
||||
IEnumerable<BaseItem> items,
|
||||
int? totalRecordLimit,
|
||||
InternalItemsQuery query,
|
||||
ILibraryManager libraryManager,
|
||||
bool enableSorting)
|
||||
ILibraryManager libraryManager)
|
||||
{
|
||||
if (enableSorting)
|
||||
if (query.OrderBy.Count > 0)
|
||||
{
|
||||
if (query.OrderBy.Count > 0)
|
||||
{
|
||||
items = libraryManager.Sort(items, query.User, query.OrderBy);
|
||||
}
|
||||
items = libraryManager.Sort(items, query.User, query.OrderBy);
|
||||
}
|
||||
|
||||
var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();
|
||||
|
||||
@@ -96,14 +96,33 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldForceSequentialOperation()
|
||||
{
|
||||
// if the user either set the setting to 1 or it's unset and we have fewer than 4 cores it's better to run sequentially.
|
||||
var fanoutSetting = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
|
||||
return fanoutSetting == 1 || (fanoutSetting <= 0 && Environment.ProcessorCount <= 3);
|
||||
}
|
||||
|
||||
private int CalculateScanConcurrencyLimit()
|
||||
{
|
||||
// when this is invoked, we already checked ShouldForceSequentialOperation for the sequential check.
|
||||
var fanoutConcurrency = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
|
||||
if (fanoutConcurrency <= 0)
|
||||
{
|
||||
// in case the user did not set a limit manually, we can assume he has 3 or more cores as already checked by ShouldForceSequentialOperation.
|
||||
return Environment.ProcessorCount - 3;
|
||||
}
|
||||
|
||||
return fanoutConcurrency;
|
||||
}
|
||||
|
||||
private void Worker()
|
||||
{
|
||||
lock (_taskLock)
|
||||
{
|
||||
var fanoutConcurrency = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
|
||||
var parallelism = (fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount) - _taskRunners.Count;
|
||||
_logger.LogDebug("Spawn {NumberRunners} new runners.", parallelism);
|
||||
for (int i = 0; i < parallelism; i++)
|
||||
var operationFanout = Math.Max(0, CalculateScanConcurrencyLimit() - _taskRunners.Count);
|
||||
_logger.LogDebug("Spawn {NumberRunners} new runners.", operationFanout);
|
||||
for (int i = 0; i < operationFanout; i++)
|
||||
{
|
||||
var stopToken = new CancellationTokenSource();
|
||||
var combinedSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, _hostApplicationLifetime.ApplicationStopping);
|
||||
@@ -223,7 +242,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr
|
||||
};
|
||||
}).ToArray();
|
||||
|
||||
if (_serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency == 1)
|
||||
if (ShouldForceSequentialOperation())
|
||||
{
|
||||
_logger.LogDebug("Process sequentially.");
|
||||
try
|
||||
|
||||
@@ -230,10 +230,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
var hwType = encodingOptions.HardwareAccelerationType;
|
||||
|
||||
// Only Intel has VA-API MJPEG encoder
|
||||
// Only enable VA-API MJPEG encoder on Intel iHD driver.
|
||||
// Legacy platforms supported ONLY by i965 do not support MJPEG encoder.
|
||||
if (hwType == HardwareAccelerationType.vaapi
|
||||
&& !(_mediaEncoder.IsVaapiDeviceInteliHD
|
||||
|| _mediaEncoder.IsVaapiDeviceInteli965))
|
||||
&& !_mediaEncoder.IsVaapiDeviceInteliHD)
|
||||
{
|
||||
return _defaultMjpegEncoder;
|
||||
}
|
||||
@@ -2390,6 +2390,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|| (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR)
|
||||
|| (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus)))
|
||||
{
|
||||
// If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG.
|
||||
if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check complicated cases where we need to remove dynamic metadata
|
||||
// Conservatively refuse to copy if the encoder can't remove dynamic metadata,
|
||||
// but a removal is required for compatability reasons.
|
||||
@@ -4442,6 +4448,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var swapOutputWandH = doVppTranspose && swapWAndH;
|
||||
var hwScaleFilter = GetHwScaleFilter("vpp", "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
|
||||
// d3d11va doesn't support dynamic pool size, use vpp filter ctx to relay
|
||||
// to prevent encoder async and bframes from exhausting the decoder pool.
|
||||
if (!string.IsNullOrEmpty(hwScaleFilter) && isD3d11vaDecoder)
|
||||
{
|
||||
hwScaleFilter += ":passthrough=0";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose)
|
||||
{
|
||||
hwScaleFilter += $":transpose={transposeDir}";
|
||||
@@ -7138,7 +7151,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
inputModifier += " -async " + state.InputAudioSync;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(state.InputVideoSync))
|
||||
// The -fps_mode option cannot be applied to input
|
||||
if (!string.IsNullOrEmpty(state.InputVideoSync) && _mediaEncoder.EncoderVersion < new Version(5, 1))
|
||||
{
|
||||
inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion);
|
||||
}
|
||||
|
||||
@@ -235,11 +235,11 @@ namespace MediaBrowser.LocalMetadata.Savers
|
||||
{
|
||||
if (item is Person)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
else if (item is not Episode)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -569,7 +569,7 @@ namespace MediaBrowser.Model.Dto
|
||||
/// Gets or sets the trickplay manifest.
|
||||
/// </summary>
|
||||
/// <value>The trickplay manifest.</value>
|
||||
public Dictionary<string, Dictionary<int, TrickplayInfo>> Trickplay { get; set; }
|
||||
public Dictionary<string, Dictionary<int, TrickplayInfoDto>> Trickplay { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the location.
|
||||
|
||||
62
MediaBrowser.Model/Dto/TrickplayInfoDto.cs
Normal file
62
MediaBrowser.Model/Dto/TrickplayInfoDto.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
|
||||
namespace MediaBrowser.Model.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// The trickplay api model.
|
||||
/// </summary>
|
||||
public record TrickplayInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TrickplayInfoDto"/> class.
|
||||
/// </summary>
|
||||
/// <param name="info">The trickplay info.</param>
|
||||
public TrickplayInfoDto(TrickplayInfo info)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(info);
|
||||
|
||||
Width = info.Width;
|
||||
Height = info.Height;
|
||||
TileWidth = info.TileWidth;
|
||||
TileHeight = info.TileHeight;
|
||||
ThumbnailCount = info.ThumbnailCount;
|
||||
Interval = info.Interval;
|
||||
Bandwidth = info.Bandwidth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of an individual thumbnail.
|
||||
/// </summary>
|
||||
public int Width { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of an individual thumbnail.
|
||||
/// </summary>
|
||||
public int Height { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of thumbnails per row.
|
||||
/// </summary>
|
||||
public int TileWidth { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of thumbnails per column.
|
||||
/// </summary>
|
||||
public int TileHeight { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total amount of non-black thumbnails.
|
||||
/// </summary>
|
||||
public int ThumbnailCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the interval in milliseconds between each trickplay thumbnail.
|
||||
/// </summary>
|
||||
public int Interval { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the peak bandwidth usage in bits per second.
|
||||
/// </summary>
|
||||
public int Bandwidth { get; init; }
|
||||
}
|
||||
@@ -8,21 +8,28 @@ public class LyricLineCue
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LyricLineCue"/> class.
|
||||
/// </summary>
|
||||
/// <param name="position">The start of the character index of the lyric.</param>
|
||||
/// <param name="position">The start character index of the cue.</param>
|
||||
/// <param name="endPosition">The end character index of the cue.</param>
|
||||
/// <param name="start">The start of the timestamp the lyric is synced to in ticks.</param>
|
||||
/// <param name="end">The end of the timestamp the lyric is synced to in ticks.</param>
|
||||
public LyricLineCue(int position, long start, long? end)
|
||||
public LyricLineCue(int position, int endPosition, long start, long? end)
|
||||
{
|
||||
Position = position;
|
||||
EndPosition = endPosition;
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the character index of the lyric.
|
||||
/// Gets the start character index of the cue.
|
||||
/// </summary>
|
||||
public int Position { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end character index of the cue.
|
||||
/// </summary>
|
||||
public int EndPosition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp the lyric is synced to in ticks.
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Jellyfin.Extensions;
|
||||
using LrcParser.Model;
|
||||
@@ -66,47 +67,56 @@ public partial class LrcLyricParser : ILyricParser
|
||||
}
|
||||
|
||||
List<LyricLine> lyricList = [];
|
||||
for (var l = 0; l < sortedLyricData.Count; l++)
|
||||
for (var lineIndex = 0; lineIndex < sortedLyricData.Count; lineIndex++)
|
||||
{
|
||||
var cues = new List<LyricLineCue>();
|
||||
var lyric = sortedLyricData[l];
|
||||
var lyric = sortedLyricData[lineIndex];
|
||||
|
||||
if (lyric.TimeTags.Count != 0)
|
||||
// Extract cues from time tags
|
||||
var cues = new List<LyricLineCue>();
|
||||
if (lyric.TimeTags.Count > 0)
|
||||
{
|
||||
var keys = lyric.TimeTags.Keys.ToList();
|
||||
int current = 0, next = 1;
|
||||
while (next < keys.Count)
|
||||
for (var tagIndex = 0; tagIndex < keys.Count - 1; tagIndex++)
|
||||
{
|
||||
var currentKey = keys[current];
|
||||
var currentKey = keys[tagIndex];
|
||||
var nextKey = keys[tagIndex + 1];
|
||||
|
||||
var currentPos = currentKey.State == IndexState.End ? currentKey.Index + 1 : currentKey.Index;
|
||||
var nextPos = nextKey.State == IndexState.End ? nextKey.Index + 1 : nextKey.Index;
|
||||
var currentMs = lyric.TimeTags[currentKey] ?? 0;
|
||||
var nextMs = lyric.TimeTags[keys[next]] ?? 0;
|
||||
|
||||
cues.Add(new LyricLineCue(
|
||||
position: Math.Max(currentKey.Index, 0),
|
||||
start: TimeSpan.FromMilliseconds(currentMs).Ticks,
|
||||
end: TimeSpan.FromMilliseconds(nextMs).Ticks));
|
||||
|
||||
current++;
|
||||
next++;
|
||||
var nextMs = lyric.TimeTags[keys[tagIndex + 1]] ?? 0;
|
||||
var currentSlice = lyric.Text[currentPos..nextPos];
|
||||
var currentSliceTrimmed = currentSlice.Trim();
|
||||
if (currentSliceTrimmed.Length > 0)
|
||||
{
|
||||
cues.Add(new LyricLineCue(
|
||||
position: currentPos,
|
||||
endPosition: nextPos,
|
||||
start: TimeSpan.FromMilliseconds(currentMs).Ticks,
|
||||
end: TimeSpan.FromMilliseconds(nextMs).Ticks));
|
||||
}
|
||||
}
|
||||
|
||||
var lastKey = keys[current];
|
||||
var lastKey = keys[^1];
|
||||
var lastPos = lastKey.State == IndexState.End ? lastKey.Index + 1 : lastKey.Index;
|
||||
var lastMs = lyric.TimeTags[lastKey] ?? 0;
|
||||
var lastSlice = lyric.Text[lastPos..];
|
||||
var lastSliceTrimmed = lastSlice.Trim();
|
||||
|
||||
cues.Add(new LyricLineCue(
|
||||
position: Math.Max(lastKey.Index, 0),
|
||||
start: TimeSpan.FromMilliseconds(lastMs).Ticks,
|
||||
end: l + 1 < sortedLyricData.Count ? TimeSpan.FromMilliseconds(sortedLyricData[l + 1].StartTime).Ticks : null));
|
||||
if (lastSliceTrimmed.Length > 0)
|
||||
{
|
||||
cues.Add(new LyricLineCue(
|
||||
position: lastPos,
|
||||
endPosition: lyric.Text.Length,
|
||||
start: TimeSpan.FromMilliseconds(lastMs).Ticks,
|
||||
end: lineIndex + 1 < sortedLyricData.Count ? TimeSpan.FromMilliseconds(sortedLyricData[lineIndex + 1].StartTime).Ticks : null));
|
||||
}
|
||||
}
|
||||
|
||||
long lyricStartTicks = TimeSpan.FromMilliseconds(lyric.StartTime).Ticks;
|
||||
lyricList.Add(new LyricLine(WhitespaceRegex().Replace(lyric.Text.Trim(), " "), lyricStartTicks, cues));
|
||||
lyricList.Add(new LyricLine(lyric.Text, lyricStartTicks, cues));
|
||||
}
|
||||
|
||||
return new LyricDto { Lyrics = lyricList };
|
||||
}
|
||||
|
||||
// Replacement is required until https://github.com/karaoke-dev/LrcParser/issues/83 is resolved.
|
||||
[GeneratedRegex(@"\s+")]
|
||||
private static partial Regex WhitespaceRegex();
|
||||
}
|
||||
|
||||
@@ -73,11 +73,11 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
public virtual int Order => 0;
|
||||
|
||||
private FileSystemMetadata TryGetFile(string path, IDirectoryService directoryService)
|
||||
private FileSystemMetadata TryGetFileSystemMetadata(string path, IDirectoryService directoryService)
|
||||
{
|
||||
try
|
||||
{
|
||||
return directoryService.GetFile(path);
|
||||
return directoryService.GetFileSystemEntry(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -92,7 +92,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
var updateType = ItemUpdateType.None;
|
||||
|
||||
var libraryOptions = LibraryManager.GetLibraryOptions(item);
|
||||
var isFirstRefresh = item.DateLastRefreshed.Date == DateTime.MinValue.Date;
|
||||
var isFirstRefresh = item.DateLastRefreshed == DateTime.MinValue;
|
||||
var hasRefreshedMetadata = true;
|
||||
var hasRefreshedImages = true;
|
||||
|
||||
@@ -225,7 +225,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
var file = TryGetFile(item.Path, refreshOptions.DirectoryService);
|
||||
var file = TryGetFileSystemMetadata(item.Path, refreshOptions.DirectoryService);
|
||||
if (file is not null)
|
||||
{
|
||||
item.DateModified = file.LastWriteTimeUtc;
|
||||
@@ -1180,12 +1180,12 @@ namespace MediaBrowser.Providers.Manager
|
||||
target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
|
||||
}
|
||||
|
||||
if (source.DateCreated != default)
|
||||
if (source.DateCreated != DateTime.MinValue)
|
||||
{
|
||||
target.DateCreated = source.DateCreated;
|
||||
}
|
||||
|
||||
if (replaceData || source.DateModified != default)
|
||||
if (replaceData || source.DateModified != DateTime.MinValue)
|
||||
{
|
||||
target.DateModified = source.DateModified;
|
||||
}
|
||||
|
||||
@@ -669,8 +669,13 @@ namespace MediaBrowser.Providers.Manager
|
||||
private async Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers)
|
||||
{
|
||||
var libraryOptions = _libraryManager.GetLibraryOptions(item);
|
||||
var applicableSavers = savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, updateType, false)).ToList();
|
||||
if (applicableSavers.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var saver in savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, updateType, false)))
|
||||
foreach (var saver in applicableSavers)
|
||||
{
|
||||
_logger.LogDebug("Saving {Item} to {Saver}", item.Path ?? item.Name, saver.Name);
|
||||
|
||||
@@ -714,6 +719,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_libraryManager.CreateItem(item, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -192,7 +192,20 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
||||
{
|
||||
var people = new List<PersonInfo>();
|
||||
var albumArtists = string.IsNullOrEmpty(trackAlbumArtist) ? [] : trackAlbumArtist.Split(InternalValueSeparator);
|
||||
string[]? albumArtists = null;
|
||||
if (libraryOptions.PreferNonstandardArtistsTag)
|
||||
{
|
||||
TryGetSanitizedAdditionalFields(track, "ALBUMARTISTS", out var albumArtistsTagString);
|
||||
if (albumArtistsTagString is not null)
|
||||
{
|
||||
albumArtists = albumArtistsTagString.Split(InternalValueSeparator);
|
||||
}
|
||||
}
|
||||
|
||||
if (albumArtists is null || albumArtists.Length == 0)
|
||||
{
|
||||
albumArtists = string.IsNullOrEmpty(trackAlbumArtist) ? [] : trackAlbumArtist.Split(InternalValueSeparator);
|
||||
}
|
||||
|
||||
if (libraryOptions.UseCustomTagDelimiters)
|
||||
{
|
||||
@@ -205,7 +218,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = albumArtist.Trim(),
|
||||
Name = albumArtist,
|
||||
Type = PersonKind.AlbumArtist
|
||||
});
|
||||
}
|
||||
@@ -237,7 +250,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = performer.Trim(),
|
||||
Name = performer,
|
||||
Type = PersonKind.Artist
|
||||
});
|
||||
}
|
||||
@@ -251,7 +264,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = composer.Trim(),
|
||||
Name = composer,
|
||||
Type = PersonKind.Composer
|
||||
});
|
||||
}
|
||||
@@ -340,9 +353,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
genres = genres.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
|
||||
audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0
|
||||
? genres
|
||||
: audio.Genres;
|
||||
if (options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0 || audio.Genres.All(string.IsNullOrWhiteSpace))
|
||||
{
|
||||
audio.Genres = genres;
|
||||
}
|
||||
}
|
||||
|
||||
TryGetSanitizedAdditionalFields(track, "REPLAYGAIN_TRACK_GAIN", out var trackGainTag);
|
||||
@@ -435,7 +449,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
// Save extracted lyrics if they exist,
|
||||
// and if the audio doesn't yet have lyrics.
|
||||
var lyrics = track.Lyrics.SynchronizedLyrics.Count > 0 ? track.Lyrics.FormatSynchToLRC() : track.Lyrics.UnsynchronizedLyrics;
|
||||
// ATL supports both SRT and LRC formats as synchronized lyrics, but we only want to save LRC format.
|
||||
var supportedLyrics = track.Lyrics.Where(l => l.Format != LyricsInfo.LyricsFormat.SRT).ToList();
|
||||
var candidateSynchronizedLyric = supportedLyrics.FirstOrDefault(l => l.Format is not LyricsInfo.LyricsFormat.UNSYNCHRONIZED and not LyricsInfo.LyricsFormat.OTHER && l.SynchronizedLyrics is not null);
|
||||
var candidateUnsynchronizedLyric = supportedLyrics.FirstOrDefault(l => l.Format is LyricsInfo.LyricsFormat.UNSYNCHRONIZED or LyricsInfo.LyricsFormat.OTHER && l.UnsynchronizedLyrics is not null);
|
||||
var lyrics = candidateSynchronizedLyric is not null ? candidateSynchronizedLyric.FormatSynch() : candidateUnsynchronizedLyric?.UnsynchronizedLyrics;
|
||||
if (!string.IsNullOrWhiteSpace(lyrics)
|
||||
&& tryExtractEmbeddedLyrics)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
@@ -516,12 +517,15 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
foreach (var person in data.People)
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
if (!string.IsNullOrWhiteSpace(person.Name))
|
||||
{
|
||||
Name = person.Name.Trim(),
|
||||
Type = person.Type,
|
||||
Role = person.Role.Trim()
|
||||
});
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = person.Name,
|
||||
Type = person.Type,
|
||||
Role = person.Role.Trim()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_libraryManager.UpdatePeople(video, people);
|
||||
|
||||
@@ -200,20 +200,26 @@ public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
|
||||
|
||||
foreach (var albumArtist in item.AlbumArtists)
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
if (!string.IsNullOrWhiteSpace(albumArtist))
|
||||
{
|
||||
Name = albumArtist.Trim(),
|
||||
Type = PersonKind.AlbumArtist
|
||||
});
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = albumArtist,
|
||||
Type = PersonKind.AlbumArtist
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var artist in item.Artists)
|
||||
{
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
if (!string.IsNullOrWhiteSpace(artist))
|
||||
{
|
||||
Name = artist.Trim(),
|
||||
Type = PersonKind.Artist
|
||||
});
|
||||
PeopleHelper.AddPerson(people, new PersonInfo
|
||||
{
|
||||
Name = artist,
|
||||
Type = PersonKind.Artist
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LibraryManager.UpdatePeople(item, people);
|
||||
|
||||
@@ -38,6 +38,21 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
/// </summary>
|
||||
public int MaxCastMembers { get; set; } = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the maximum number of crew members to fetch for an item.
|
||||
/// </summary>
|
||||
public int MaxCrewMembers { get; set; } = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to hide cast members without profile images.
|
||||
/// </summary>
|
||||
public bool HideMissingCastMembers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to hide crew members without profile images.
|
||||
/// </summary>
|
||||
public bool HideMissingCrewMembers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the poster image size to fetch.
|
||||
/// </summary>
|
||||
|
||||
@@ -25,9 +25,24 @@
|
||||
<input is="emby-checkbox" type="checkbox" id="importSeasonName" />
|
||||
<span>Import season name from metadata fetched for series.</span>
|
||||
</label>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="maxCastMembers" pattern="[0-9]*" required min="0" max="1000" label="Max Cast Members" />
|
||||
<div class="fieldDescription">The maximum number of cast members to fetch for an item.</div>
|
||||
<div class="verticalSection">
|
||||
<h2>Cast & Crew Settings</h2>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="maxCastMembers" pattern="[0-9]*" required min="0" max="1000" label="Max Cast Members" />
|
||||
<div class="fieldDescription">The maximum number of cast members to fetch for an item.</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="maxCrewMembers" pattern="[0-9]*" required min="0" max="1000" label="Max Crew Members" />
|
||||
<div class="fieldDescription">The maximum number of crew members to fetch for an item.</div>
|
||||
</div>
|
||||
<label class="checkboxContainer">
|
||||
<input is="emby-checkbox" type="checkbox" id="hideMissingCastMembers" />
|
||||
<span>Hide cast members without profile images.</span>
|
||||
</label>
|
||||
<label class="checkboxContainer">
|
||||
<input is="emby-checkbox" type="checkbox" id="hideMissingCrewMembers" />
|
||||
<span>Hide crew members without profile images.</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<h2>Image Scaling</h2>
|
||||
@@ -129,6 +144,8 @@
|
||||
document.querySelector('#excludeTagsSeries').checked = config.ExcludeTagsSeries;
|
||||
document.querySelector('#excludeTagsMovies').checked = config.ExcludeTagsMovies;
|
||||
document.querySelector('#importSeasonName').checked = config.ImportSeasonName;
|
||||
document.querySelector('#hideMissingCastMembers').checked = config.HideMissingCastMembers;
|
||||
document.querySelector('#hideMissingCrewMembers').checked = config.HideMissingCrewMembers;
|
||||
|
||||
var maxCastMembers = document.querySelector('#maxCastMembers');
|
||||
maxCastMembers.value = config.MaxCastMembers;
|
||||
@@ -137,12 +154,18 @@
|
||||
cancelable: false
|
||||
}));
|
||||
|
||||
var maxCrewMembers = document.querySelector('#maxCrewMembers');
|
||||
maxCrewMembers.value = config.MaxCrewMembers;
|
||||
maxCrewMembers.dispatchEvent(new Event('change', {
|
||||
bubbles: true,
|
||||
cancelable: false
|
||||
}));
|
||||
|
||||
pluginConfig = config;
|
||||
configureImageScaling();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.querySelector('.configForm')
|
||||
.addEventListener('submit', function (e) {
|
||||
Dashboard.showLoadingMsg();
|
||||
@@ -153,6 +176,9 @@
|
||||
config.ExcludeTagsMovies = document.querySelector('#excludeTagsMovies').checked;
|
||||
config.ImportSeasonName = document.querySelector('#importSeasonName').checked;
|
||||
config.MaxCastMembers = document.querySelector('#maxCastMembers').value;
|
||||
config.MaxCrewMembers = document.querySelector('#maxCrewMembers').value;
|
||||
config.HideMissingCastMembers = document.querySelector('#hideMissingCastMembers').checked;
|
||||
config.HideMissingCrewMembers = document.querySelector('#hideMissingCrewMembers').checked;
|
||||
config.PosterSize = document.querySelector('#selectPosterSize').value;
|
||||
config.BackdropSize = document.querySelector('#selectBackdropSize').value;
|
||||
config.LogoSize = document.querySelector('#selectLogoSize').value;
|
||||
|
||||
@@ -144,6 +144,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
{
|
||||
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
|
||||
var imdbId = info.GetProviderId(MetadataProvider.Imdb);
|
||||
var config = Plugin.Instance.Configuration;
|
||||
|
||||
if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
@@ -249,12 +250,26 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
|
||||
if (movieResult.Credits?.Cast is not null)
|
||||
{
|
||||
foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
|
||||
var castQuery = movieResult.Credits.Cast.AsEnumerable();
|
||||
|
||||
if (config.HideMissingCastMembers)
|
||||
{
|
||||
castQuery = castQuery.Where(a => !string.IsNullOrEmpty(a.ProfilePath));
|
||||
}
|
||||
|
||||
castQuery = castQuery.OrderBy(a => a.Order).Take(config.MaxCastMembers);
|
||||
|
||||
foreach (var actor in castQuery)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(actor.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = actor.Name.Trim(),
|
||||
Role = actor.Character.Trim(),
|
||||
Role = actor.Character?.Trim() ?? string.Empty,
|
||||
Type = PersonKind.Actor,
|
||||
SortOrder = actor.Order
|
||||
};
|
||||
@@ -275,32 +290,47 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
|
||||
if (movieResult.Credits?.Crew is not null)
|
||||
{
|
||||
foreach (var person in movieResult.Credits.Crew)
|
||||
{
|
||||
// Normalize this
|
||||
var type = TmdbUtils.MapCrewToPersonType(person);
|
||||
var crewQuery = movieResult.Credits.Crew
|
||||
.Select(crewMember => new
|
||||
{
|
||||
CrewMember = crewMember,
|
||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||
})
|
||||
.Where(entry =>
|
||||
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!TmdbUtils.WantedCrewKinds.Contains(type)
|
||||
&& !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
if (config.HideMissingCrewMembers)
|
||||
{
|
||||
crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
|
||||
}
|
||||
|
||||
crewQuery = crewQuery.Take(config.MaxCrewMembers);
|
||||
|
||||
foreach (var entry in crewQuery)
|
||||
{
|
||||
var crewMember = entry.CrewMember;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(crewMember.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = person.Name.Trim(),
|
||||
Role = person.Job?.Trim(),
|
||||
Type = type
|
||||
Name = crewMember.Name.Trim(),
|
||||
Role = crewMember.Job?.Trim() ?? string.Empty,
|
||||
Type = entry.PersonType
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(person.ProfilePath))
|
||||
if (!string.IsNullOrWhiteSpace(crewMember.ProfilePath))
|
||||
{
|
||||
personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(person.ProfilePath);
|
||||
personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath);
|
||||
}
|
||||
|
||||
if (person.Id > 0)
|
||||
if (crewMember.Id > 0)
|
||||
{
|
||||
personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
|
||||
personInfo.SetProviderId(MetadataProvider.Tmdb, crewMember.Id.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
metadataResult.AddPerson(personInfo);
|
||||
|
||||
@@ -81,6 +81,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var metadataResult = new MetadataResult<Episode>();
|
||||
var config = Plugin.Instance.Configuration;
|
||||
|
||||
// Allowing this will dramatically increase scan times
|
||||
if (info.IsMissingEpisode)
|
||||
@@ -206,52 +207,106 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
if (credits?.Cast is not null)
|
||||
{
|
||||
foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
|
||||
var castQuery = config.HideMissingCastMembers
|
||||
? credits.Cast.Where(a => !string.IsNullOrEmpty(a.ProfilePath)).OrderBy(a => a.Order)
|
||||
: credits.Cast.OrderBy(a => a.Order);
|
||||
|
||||
foreach (var actor in castQuery.Take(config.MaxCastMembers))
|
||||
{
|
||||
metadataResult.AddPerson(new PersonInfo
|
||||
if (string.IsNullOrWhiteSpace(actor.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = actor.Name.Trim(),
|
||||
Role = actor.Character.Trim(),
|
||||
Role = actor.Character?.Trim() ?? string.Empty,
|
||||
Type = PersonKind.Actor,
|
||||
SortOrder = actor.Order
|
||||
});
|
||||
SortOrder = actor.Order,
|
||||
ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath)
|
||||
};
|
||||
|
||||
if (actor.Id > 0)
|
||||
{
|
||||
personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
metadataResult.AddPerson(personInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (credits?.GuestStars is not null)
|
||||
{
|
||||
foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
|
||||
{
|
||||
metadataResult.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = guest.Name.Trim(),
|
||||
Role = guest.Character.Trim(),
|
||||
Type = PersonKind.GuestStar,
|
||||
SortOrder = guest.Order
|
||||
});
|
||||
}
|
||||
}
|
||||
var guestQuery = config.HideMissingCastMembers
|
||||
? credits.GuestStars.Where(a => !string.IsNullOrEmpty(a.ProfilePath)).OrderBy(a => a.Order)
|
||||
: credits.GuestStars.OrderBy(a => a.Order);
|
||||
|
||||
// and the rest from crew
|
||||
if (credits?.Crew is not null)
|
||||
{
|
||||
foreach (var person in credits.Crew)
|
||||
foreach (var guest in guestQuery.Take(config.MaxCastMembers))
|
||||
{
|
||||
// Normalize this
|
||||
var type = TmdbUtils.MapCrewToPersonType(person);
|
||||
|
||||
if (!TmdbUtils.WantedCrewKinds.Contains(type)
|
||||
&& !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.IsNullOrWhiteSpace(guest.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
metadataResult.AddPerson(new PersonInfo
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = person.Name.Trim(),
|
||||
Role = person.Job?.Trim(),
|
||||
Type = type
|
||||
});
|
||||
Name = guest.Name.Trim(),
|
||||
Role = guest.Character?.Trim() ?? string.Empty,
|
||||
Type = PersonKind.GuestStar,
|
||||
SortOrder = guest.Order,
|
||||
ImageUrl = _tmdbClientManager.GetProfileUrl(guest.ProfilePath)
|
||||
};
|
||||
|
||||
if (guest.Id > 0)
|
||||
{
|
||||
personInfo.SetProviderId(MetadataProvider.Tmdb, guest.Id.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
metadataResult.AddPerson(personInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (credits?.Crew is not null)
|
||||
{
|
||||
var crewQuery = credits.Crew
|
||||
.Select(crewMember => new
|
||||
{
|
||||
CrewMember = crewMember,
|
||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||
})
|
||||
.Where(entry =>
|
||||
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (config.HideMissingCrewMembers)
|
||||
{
|
||||
crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
|
||||
}
|
||||
|
||||
foreach (var entry in crewQuery.Take(config.MaxCrewMembers))
|
||||
{
|
||||
var crewMember = entry.CrewMember;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(crewMember.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = crewMember.Name.Trim(),
|
||||
Role = crewMember.Job?.Trim() ?? string.Empty,
|
||||
Type = entry.PersonType,
|
||||
ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath)
|
||||
};
|
||||
|
||||
if (crewMember.Id > 0)
|
||||
{
|
||||
personInfo.SetProviderId(MetadataProvider.Tmdb, crewMember.Id.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
metadataResult.AddPerson(personInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
public async Task<MetadataResult<Season>> GetMetadata(SeasonInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new MetadataResult<Season>();
|
||||
var config = Plugin.Instance.Configuration;
|
||||
|
||||
info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string? seriesTmdbId);
|
||||
|
||||
@@ -65,10 +66,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
result.Item = new Season
|
||||
{
|
||||
IndexNumber = seasonNumber,
|
||||
Overview = seasonResult.Overview
|
||||
Overview = seasonResult.Overview,
|
||||
PremiereDate = seasonResult.AirDate,
|
||||
ProductionYear = seasonResult.AirDate?.Year
|
||||
};
|
||||
|
||||
if (Plugin.Instance.Configuration.ImportSeasonName)
|
||||
if (config.ImportSeasonName)
|
||||
{
|
||||
result.Item.Name = seasonResult.Name;
|
||||
}
|
||||
@@ -77,47 +80,81 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
// TODO why was this disabled?
|
||||
var credits = seasonResult.Credits;
|
||||
|
||||
if (credits?.Cast is not null)
|
||||
{
|
||||
var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList();
|
||||
for (var i = 0; i < cast.Count; i++)
|
||||
var castQuery = config.HideMissingCastMembers
|
||||
? credits.Cast.Where(a => !string.IsNullOrEmpty(a.ProfilePath)).OrderBy(a => a.Order)
|
||||
: credits.Cast.OrderBy(a => a.Order);
|
||||
|
||||
foreach (var actor in castQuery.Take(config.MaxCastMembers))
|
||||
{
|
||||
var member = cast[i];
|
||||
result.AddPerson(new PersonInfo
|
||||
if (string.IsNullOrWhiteSpace(actor.Name))
|
||||
{
|
||||
Name = member.Name.Trim(),
|
||||
Role = member.Character.Trim(),
|
||||
continue;
|
||||
}
|
||||
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = actor.Name.Trim(),
|
||||
Role = actor.Character?.Trim() ?? string.Empty,
|
||||
Type = PersonKind.Actor,
|
||||
SortOrder = member.Order
|
||||
});
|
||||
SortOrder = actor.Order,
|
||||
ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath)
|
||||
};
|
||||
|
||||
if (actor.Id > 0)
|
||||
{
|
||||
personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
result.AddPerson(personInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (credits?.Crew is not null)
|
||||
{
|
||||
foreach (var person in credits.Crew)
|
||||
{
|
||||
// Normalize this
|
||||
var type = TmdbUtils.MapCrewToPersonType(person);
|
||||
var crewQuery = credits.Crew
|
||||
.Select(crewMember => new
|
||||
{
|
||||
CrewMember = crewMember,
|
||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||
})
|
||||
.Where(entry =>
|
||||
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!TmdbUtils.WantedCrewKinds.Contains(type)
|
||||
&& !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
if (config.HideMissingCrewMembers)
|
||||
{
|
||||
crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
|
||||
}
|
||||
|
||||
foreach (var entry in crewQuery.Take(config.MaxCrewMembers))
|
||||
{
|
||||
var crewMember = entry.CrewMember;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(crewMember.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.AddPerson(new PersonInfo
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = person.Name.Trim(),
|
||||
Role = person.Job?.Trim(),
|
||||
Type = type
|
||||
});
|
||||
Name = crewMember.Name.Trim(),
|
||||
Role = crewMember.Job?.Trim() ?? string.Empty,
|
||||
Type = entry.PersonType,
|
||||
ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath)
|
||||
};
|
||||
|
||||
if (crewMember.Id > 0)
|
||||
{
|
||||
personInfo.SetProviderId(MetadataProvider.Tmdb, crewMember.Id.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
result.AddPerson(personInfo);
|
||||
}
|
||||
}
|
||||
|
||||
result.Item.PremiereDate = seasonResult.AirDate;
|
||||
result.Item.ProductionYear = seasonResult.AirDate?.Year;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -323,17 +323,31 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
private IEnumerable<PersonInfo> GetPersons(TvShow seriesResult)
|
||||
{
|
||||
var config = Plugin.Instance.Configuration;
|
||||
|
||||
if (seriesResult.Credits?.Cast is not null)
|
||||
{
|
||||
foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
|
||||
IEnumerable<Cast> castQuery = seriesResult.Credits.Cast.OrderBy(a => a.Order);
|
||||
|
||||
if (config.HideMissingCastMembers)
|
||||
{
|
||||
castQuery = castQuery.Where(a => !string.IsNullOrEmpty(a.ProfilePath));
|
||||
}
|
||||
|
||||
foreach (var actor in castQuery.Take(config.MaxCastMembers))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(actor.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = actor.Name.Trim(),
|
||||
Role = actor.Character.Trim(),
|
||||
Role = actor.Character?.Trim() ?? string.Empty,
|
||||
Type = PersonKind.Actor,
|
||||
SortOrder = actor.Order,
|
||||
ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath)
|
||||
ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath)
|
||||
};
|
||||
|
||||
if (actor.Id > 0)
|
||||
@@ -347,30 +361,44 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
||||
|
||||
if (seriesResult.Credits?.Crew is not null)
|
||||
{
|
||||
var keepTypes = new[]
|
||||
{
|
||||
PersonType.Director,
|
||||
PersonType.Writer,
|
||||
PersonType.Producer
|
||||
};
|
||||
var crewQuery = seriesResult.Credits.Crew
|
||||
.Select(crewMember => new
|
||||
{
|
||||
CrewMember = crewMember,
|
||||
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
|
||||
})
|
||||
.Where(entry =>
|
||||
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
|
||||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
foreach (var person in seriesResult.Credits.Crew)
|
||||
if (config.HideMissingCrewMembers)
|
||||
{
|
||||
// Normalize this
|
||||
var type = TmdbUtils.MapCrewToPersonType(person);
|
||||
crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
|
||||
}
|
||||
|
||||
if (!TmdbUtils.WantedCrewKinds.Contains(type)
|
||||
&& !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
foreach (var entry in crewQuery.Take(config.MaxCrewMembers))
|
||||
{
|
||||
var crewMember = entry.CrewMember;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(crewMember.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new PersonInfo
|
||||
var personInfo = new PersonInfo
|
||||
{
|
||||
Name = person.Name.Trim(),
|
||||
Role = person.Job?.Trim(),
|
||||
Type = type
|
||||
Name = crewMember.Name.Trim(),
|
||||
Role = crewMember.Job?.Trim() ?? string.Empty,
|
||||
Type = entry.PersonType,
|
||||
ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath)
|
||||
};
|
||||
|
||||
if (crewMember.Id > 0)
|
||||
{
|
||||
personInfo.SetProviderId(MetadataProvider.Tmdb, crewMember.Id.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
yield return personInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -22,7 +22,7 @@ public class BaseItemImageInfo
|
||||
/// <summary>
|
||||
/// Gets or Sets the time the image was last modified.
|
||||
/// </summary>
|
||||
public DateTime DateModified { get; set; }
|
||||
public DateTime? DateModified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the imagetype.
|
||||
|
||||
@@ -82,7 +82,7 @@ public static class QueryPartitionHelpers
|
||||
/// <typeparam name="TEntity">The entity to load.</typeparam>
|
||||
/// <param name="partitionInfo">The source query.</param>
|
||||
/// <param name="partitionSize">The number of elements to load per partition.</param>
|
||||
/// <param name="cancellationToken">The cancelation token.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A enumerable representing the whole of the query.</returns>
|
||||
public static async IAsyncEnumerable<TEntity> PartitionAsync<TEntity>(this ProgressablePartitionReporting<TEntity> partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -98,7 +98,7 @@ public static class QueryPartitionHelpers
|
||||
/// <typeparam name="TEntity">The entity to load.</typeparam>
|
||||
/// <param name="partitionInfo">The source query.</param>
|
||||
/// <param name="partitionSize">The number of elements to load per partition.</param>
|
||||
/// <param name="cancellationToken">The cancelation token.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A enumerable representing the whole of the query.</returns>
|
||||
public static async IAsyncEnumerable<TEntity> PartitionEagerAsync<TEntity>(this ProgressablePartitionReporting<TEntity> partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -115,7 +115,7 @@ public static class QueryPartitionHelpers
|
||||
/// <param name="query">The source query.</param>
|
||||
/// <param name="partitionSize">The number of elements to load per partition.</param>
|
||||
/// <param name="progressablePartition">Reporting helper.</param>
|
||||
/// <param name="cancellationToken">The cancelation token.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A enumerable representing the whole of the query.</returns>
|
||||
public static async IAsyncEnumerable<TEntity> PartitionAsync<TEntity>(
|
||||
this IOrderedQueryable<TEntity> query,
|
||||
@@ -154,7 +154,7 @@ public static class QueryPartitionHelpers
|
||||
/// <param name="query">The source query.</param>
|
||||
/// <param name="partitionSize">The number of elements to load per partition.</param>
|
||||
/// <param name="progressablePartition">Reporting helper.</param>
|
||||
/// <param name="cancellationToken">The cancelation token.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A enumerable representing the whole of the query.</returns>
|
||||
public static async IAsyncEnumerable<TEntity> PartitionEagerAsync<TEntity>(
|
||||
this IOrderedQueryable<TEntity> query,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class BaseItemImageInfoDateModifiedNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "DateModified",
|
||||
table: "BaseItemImageInfos",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "TEXT");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<DateTime>(
|
||||
name: "DateModified",
|
||||
table: "BaseItemImageInfos",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
oldClrType: typeof(DateTime),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +418,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<byte[]>("Blurhash")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("DateModified")
|
||||
b.Property<DateTime?>("DateModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Height")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia;
|
||||
@@ -27,12 +28,17 @@ public static class SkiaHelper
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
SKBitmap? bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
|
||||
|
||||
var imagePath = paths[currentIndex];
|
||||
imagesTested[currentIndex] = 0;
|
||||
|
||||
currentIndex++;
|
||||
|
||||
if (!Path.Exists(imagePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SKBitmap? bitmap = skiaEncoder.Decode(imagePath, false, null, out _);
|
||||
|
||||
if (bitmap is not null)
|
||||
{
|
||||
newIndex = currentIndex;
|
||||
|
||||
@@ -1166,7 +1166,7 @@ namespace Jellyfin.LiveTv.Channels
|
||||
}
|
||||
}
|
||||
|
||||
if (isNew || forceUpdate || item.DateLastRefreshed == default)
|
||||
if (isNew || forceUpdate || item.DateLastRefreshed == DateTime.MinValue)
|
||||
{
|
||||
_providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
|
||||
var videos = _libraryManager.GetItemList(query);
|
||||
foreach (var video in videos)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Only local files supported
|
||||
var path = video.Path;
|
||||
if (File.Exists(path))
|
||||
|
||||
@@ -20,22 +20,28 @@ public static class LrcLyricParserTests
|
||||
var line1 = parsed.Lyrics[0];
|
||||
Assert.Equal("Every night that goes between", line1.Text);
|
||||
Assert.NotNull(line1.Cues);
|
||||
Assert.Equal(9, line1.Cues.Count);
|
||||
Assert.Equal(5, line1.Cues.Count);
|
||||
Assert.Equal(68400000, line1.Cues[0].Start);
|
||||
Assert.Equal(72000000, line1.Cues[0].End);
|
||||
Assert.Equal(0, line1.Cues[0].Position);
|
||||
Assert.Equal(5, line1.Cues[0].EndPosition);
|
||||
Assert.Equal(6, line1.Cues[1].Position);
|
||||
Assert.Equal(11, line1.Cues[1].EndPosition);
|
||||
Assert.Equal(12, line1.Cues[2].Position);
|
||||
|
||||
var line5 = parsed.Lyrics[4];
|
||||
Assert.Equal("Every night you do not come", line5.Text);
|
||||
Assert.NotNull(line5.Cues);
|
||||
Assert.Equal(11, line5.Cues.Count);
|
||||
Assert.Equal(377300000, line5.Cues[5].Start);
|
||||
Assert.Equal(380000000, line5.Cues[5].End);
|
||||
Assert.Equal(6, line5.Cues.Count);
|
||||
Assert.Equal(375200000, line5.Cues[2].Start);
|
||||
Assert.Equal(377300000, line5.Cues[2].End);
|
||||
|
||||
var lastLine = parsed.Lyrics[^1];
|
||||
Assert.Equal("I have always been a storm", lastLine.Text);
|
||||
Assert.NotNull(lastLine.Cues);
|
||||
Assert.Equal(11, lastLine.Cues.Count);
|
||||
Assert.Equal(6, lastLine.Cues.Count);
|
||||
Assert.Equal(2358000000, lastLine.Cues[^1].Start);
|
||||
Assert.Equal(26, lastLine.Cues[^1].EndPosition);
|
||||
Assert.Null(lastLine.Cues[^1].End);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Jellyfin.Providers.Tests.Manager
|
||||
{
|
||||
var newLocked = new[] { MetadataField.Genres, MetadataField.Cast };
|
||||
var newString = "new";
|
||||
var newDate = DateTime.Now;
|
||||
var newDate = DateTime.UtcNow;
|
||||
|
||||
var oldLocked = new[] { MetadataField.Genres };
|
||||
var oldString = "old";
|
||||
@@ -39,6 +39,7 @@ namespace Jellyfin.Providers.Tests.Manager
|
||||
DateCreated = newDate
|
||||
}
|
||||
};
|
||||
|
||||
if (defaultDate)
|
||||
{
|
||||
source.Item.DateCreated = default;
|
||||
@@ -141,8 +142,8 @@ namespace Jellyfin.Providers.Tests.Manager
|
||||
{ "ProductionYear", 1, 2 },
|
||||
{ "CommunityRating", 1.0f, 2.0f },
|
||||
{ "CriticRating", 1.0f, 2.0f },
|
||||
{ "EndDate", DateTime.UnixEpoch, DateTime.Now },
|
||||
{ "PremiereDate", DateTime.UnixEpoch, DateTime.Now },
|
||||
{ "EndDate", DateTime.UnixEpoch, DateTime.UtcNow },
|
||||
{ "PremiereDate", DateTime.UnixEpoch, DateTime.UtcNow },
|
||||
{ "Video3DFormat", Video3DFormat.HalfSideBySide, Video3DFormat.FullSideBySide }
|
||||
};
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
|
||||
Description = packageInfo.Description,
|
||||
Overview = packageInfo.Overview,
|
||||
TargetAbi = packageInfo.Versions[0].TargetAbi!,
|
||||
Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture),
|
||||
Timestamp = DateTimeOffset.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture).UtcDateTime,
|
||||
Changelog = packageInfo.Versions[0].Changelog!,
|
||||
Version = new Version(1, 0).ToString(),
|
||||
ImagePath = string.Empty
|
||||
@@ -221,7 +221,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
|
||||
Description = packageInfo.Description,
|
||||
Overview = packageInfo.Overview,
|
||||
TargetAbi = packageInfo.Versions[0].TargetAbi!,
|
||||
Timestamp = DateTime.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture),
|
||||
Timestamp = DateTimeOffset.Parse(packageInfo.Versions[0].Timestamp!, CultureInfo.InvariantCulture).UtcDateTime,
|
||||
Changelog = packageInfo.Versions[0].Changelog!,
|
||||
Version = packageInfo.Versions[0].Version,
|
||||
ImagePath = string.Empty
|
||||
|
||||
Reference in New Issue
Block a user