aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.devcontainer/devcontainer.json2
-rw-r--r--.github/workflows/ci-codeql-analysis.yml6
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Directory.Packages.props10
-rw-r--r--Emby.Naming/Common/NamingOptions.cs3
-rw-r--r--Emby.Photos/PhotoProvider.cs2
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs7
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs2
-rw-r--r--Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs14
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs28
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json50
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/gl.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json18
-rw-r--r--Emby.Server.Implementations/Localization/Core/id.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/kn.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/mr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ro.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/sk.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ta.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json4
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs2
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs1
-rw-r--r--Emby.Server.Implementations/Sorting/StartDateComparer.cs1
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs54
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs2
-rw-r--r--Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs (renamed from Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs)4
-rw-r--r--Jellyfin.Server/Migrations/Routines/FixDates.cs168
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs71
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs107
-rw-r--r--Jellyfin.Server/Migrations/Stages/CodeMigration.cs2
-rw-r--r--Jellyfin.Server/ServerSetupApp/SetupServer.cs5
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs15
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs16
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs4
-rw-r--r--MediaBrowser.Model/Dto/BaseItemDto.cs2
-rw-r--r--MediaBrowser.Model/Dto/TrickplayInfoDto.cs62
-rw-r--r--MediaBrowser.Model/Lyrics/LyricLineCue.cs13
-rw-r--r--MediaBrowser.Providers/Lyric/LrcLyricParser.cs62
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs12
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs9
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs7
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html34
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs58
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs97
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs83
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs62
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs1
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs8
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.Designer.cs1709
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs37
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs2
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaHelper.cs12
-rw-r--r--src/Jellyfin.LiveTv/Channels/ChannelManager.cs2
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs2
-rw-r--r--tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs16
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs7
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs4
67 files changed, 2689 insertions, 305 deletions
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index c2127ba5c..8b6b12c31 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -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"
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index cd5b11603..50c509d74 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -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
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 1a98414e9..06c72c616 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -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
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 43f11bec8..6b859bb7f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,7 +9,7 @@
<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" />
@@ -24,7 +24,7 @@
<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" />
@@ -59,7 +59,7 @@
<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.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
@@ -79,7 +79,7 @@
<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.Linq.Async" Version="6.0.3" />
<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" />
@@ -92,4 +92,4 @@
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xunit" Version="2.9.3" />
</ItemGroup>
-</Project> \ No newline at end of file
+</Project>
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index dbb848603..192235bae 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -188,7 +188,8 @@ namespace Emby.Naming.Common
"disk",
"vol",
"volume",
- "part"
+ "part",
+ "act"
};
ArtistSubfolders = new[]
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index ac6c41ca5..4aca412ba 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -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;
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 9e0a6080d..cf886ae82 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -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;
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index a720c86fb..373b0994a 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer
RemoteEndPoint = remoteEndPoint;
_jsonOptions = JsonDefaults.Options;
- LastActivityDate = DateTime.Now;
+ LastActivityDate = DateTime.UtcNow;
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 8b2869149..4874eca8e 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -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)
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 47d6663a1..6ffe67776 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2050,13 +2050,17 @@ namespace Emby.Server.Implementations.Library
/// <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 +2101,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 +2386,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 +2450,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 +2526,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 +2996,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
};
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index ab6bc4907..06aa772bd 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -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;
}
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 0e1b9e0d8..a92148caf 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -136,5 +136,7 @@
"TaskExtractMediaSegments": "فحص مقاطع الوسائط",
"TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
"TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
- "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة."
+ "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
+ "CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
+ "CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index d41aaa984..fdfcb3c3b 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index c9f580cd5..0814e6223 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -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."
}
diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json
index 55b309098..ff6f6d232 100644
--- a/Emby.Server.Implementations/Localization/Core/gl.json
+++ b/Emby.Server.Implementations/Localization/Core/gl.json
@@ -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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 1809c9d3f..90c921898 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -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": "משימת ניקוי מידע משתמש"
}
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index b925a482b..2a4281685 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index e05afbabe..421c4ee30 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -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."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 14a576592..d564d54ce 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -135,5 +135,7 @@
"TaskMoveTrickplayImages": "Trickplayの画像を移動",
"TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。",
"TaskDownloadMissingLyrics": "失われた歌詞をダウンロード",
- "TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。"
+ "TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。",
+ "CleanupUserDataTask": "ユーザーデータのクリーンアップタスク",
+ "CleanupUserDataTaskDescription": "90日以上存在しないメディアに対して、視聴状態やお気に入り状態などのユーザーデータをすべて削除します。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json
index 5e2b3756b..9f49be53b 100644
--- a/Emby.Server.Implementations/Localization/Core/kn.json
+++ b/Emby.Server.Implementations/Localization/Core/kn.json
@@ -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": "ಇಲ್ಲದ ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳಿಂದ ವಸ್ತುಗಳನ್ನು ತೆಗೆದುಹಾಕುತ್ತದೆ."
}
diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json
index 5a99d8c53..9cfeb407b 100644
--- a/Emby.Server.Implementations/Localization/Core/mr.json
+++ b/Emby.Server.Implementations/Localization/Core/mr.json
@@ -130,5 +130,7 @@
"TaskExtractMediaSegments": "मिडिया विभाग तपासणी",
"TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा",
"TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
- "TaskAudioNormalization": "ऑडिओ सामान्यीकरण"
+ "TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
+ "TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
+ "TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index a3fc7881e..8d8b32f81 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 9f4f58cb6..dc5bff161 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -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."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 6c49481c3..f188822d6 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index a873c157e..bf71c5afa 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -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."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 66d8bf899..1de78eeae 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -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í."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index 8de4e7115..defdc5925 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -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": "பயனர் தரவை சுத்தம் செய்யும் பணி"
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index a3cf78fcb..3e3ca5a1a 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -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."
}
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 8eeca3667..91ccb16ef 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -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,
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index ad91cd642..cf2ca047c 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -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))
diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
index e0b438ef1..861ca2d3a 100644
--- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
@@ -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
{
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index c6eda26af..52585c996 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -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)).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
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 3038841df..08c1a5065 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -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>();
diff --git a/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs b/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs
index 74470eda0..fef5577a1 100644
--- a/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs
+++ b/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs
@@ -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>()
{
{
diff --git a/Jellyfin.Server/Migrations/Routines/FixDates.cs b/Jellyfin.Server/Migrations/Routines/FixDates.cs
new file mode 100644
index 000000000..f112502b9
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/FixDates.cs
@@ -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();
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 0953030fa..e25c52786 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -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);
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
new file mode 100644
index 000000000..beb8c0a9b
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs
@@ -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);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs
index c3592f62a..264710bce 100644
--- a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs
+++ b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs
@@ -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)
{
diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
index 53e63f0f7..92e012940 100644
--- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs
+++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
@@ -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 =>
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 96984758f..4efa3f410 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -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()
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 26d4c4fcf..cd805f3dc 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -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);
}
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index c25adb774..025a81524 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -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);
}
}
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index 937409111..8f223c12a 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -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.
diff --git a/MediaBrowser.Model/Dto/TrickplayInfoDto.cs b/MediaBrowser.Model/Dto/TrickplayInfoDto.cs
new file mode 100644
index 000000000..0c5f6e817
--- /dev/null
+++ b/MediaBrowser.Model/Dto/TrickplayInfoDto.cs
@@ -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; }
+}
diff --git a/MediaBrowser.Model/Lyrics/LyricLineCue.cs b/MediaBrowser.Model/Lyrics/LyricLineCue.cs
index 1172a0231..291553361 100644
--- a/MediaBrowser.Model/Lyrics/LyricLineCue.cs
+++ b/MediaBrowser.Model/Lyrics/LyricLineCue.cs
@@ -8,22 +8,29 @@ 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>
public long Start { get; }
diff --git a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
index 27d17b535..fa711eb28 100644
--- a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
+++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
@@ -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 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));
+ var currentKey = keys[tagIndex];
+ var nextKey = keys[tagIndex + 1];
- current++;
- next++;
+ 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[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();
}
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index cd95bf6ef..5c855328b 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -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;
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 1a29548f2..87ca728d3 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -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>
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index cbbb7e83e..45e8553ea 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -340,9 +340,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);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
index 99b759ae2..f11b1d95a 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
@@ -39,6 +39,21 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
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>
public string? PosterSize { get; set; }
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
index f3c24e7b4..89d380ec1 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
@@ -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;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 9bb6507fe..2f8cb68ef 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -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)
+ 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 (config.HideMissingCrewMembers)
+ {
+ crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
+ }
+
+ crewQuery = crewQuery.Take(config.MaxCrewMembers);
+
+ foreach (var entry in crewQuery)
{
- // Normalize this
- var type = TmdbUtils.MapCrewToPersonType(person);
+ var crewMember = entry.CrewMember;
- if (!TmdbUtils.WantedCrewKinds.Contains(type)
- && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ 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);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index 73c3b4f16..7d0900cfd 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -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))
+ var guestQuery = config.HideMissingCastMembers
+ ? credits.GuestStars.Where(a => !string.IsNullOrEmpty(a.ProfilePath)).OrderBy(a => a.Order)
+ : credits.GuestStars.OrderBy(a => a.Order);
+
+ foreach (var guest in guestQuery.Take(config.MaxCastMembers))
{
- metadataResult.AddPerson(new PersonInfo
+ if (string.IsNullOrWhiteSpace(guest.Name))
+ {
+ continue;
+ }
+
+ var personInfo = new PersonInfo
{
Name = guest.Name.Trim(),
- Role = guest.Character.Trim(),
+ Role = guest.Character?.Trim() ?? string.Empty,
Type = PersonKind.GuestStar,
- SortOrder = guest.Order
- });
+ SortOrder = guest.Order,
+ ImageUrl = _tmdbClientManager.GetProfileUrl(guest.ProfilePath)
+ };
+
+ if (guest.Id > 0)
+ {
+ personInfo.SetProviderId(MetadataProvider.Tmdb, guest.Id.ToString(CultureInfo.InvariantCulture));
+ }
+
+ metadataResult.AddPerson(personInfo);
}
}
- // and the rest from crew
if (credits?.Crew is not null)
{
- foreach (var person in credits.Crew)
+ 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))
{
- // Normalize this
- var type = TmdbUtils.MapCrewToPersonType(person);
+ var crewMember = entry.CrewMember;
- if (!TmdbUtils.WantedCrewKinds.Contains(type)
- && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (string.IsNullOrWhiteSpace(crewMember.Name))
{
continue;
}
- metadataResult.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));
+ }
+
+ metadataResult.AddPerson(personInfo);
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index b0a1e00df..cfef0d656 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -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))
+ {
+ continue;
+ }
+
+ var personInfo = new PersonInfo
{
- Name = member.Name.Trim(),
- Role = member.Character.Trim(),
+ 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)
+ 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)
{
- // 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;
}
- 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;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index 9ace9c674..8791712c7 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -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[]
+ 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));
+
+ if (config.HideMissingCrewMembers)
{
- PersonType.Director,
- PersonType.Writer,
- PersonType.Producer
- };
+ crewQuery = crewQuery.Where(entry => !string.IsNullOrEmpty(entry.CrewMember.ProfilePath));
+ }
- foreach (var person in seriesResult.Credits.Crew)
+ foreach (var entry in crewQuery.Take(config.MaxCrewMembers))
{
- // Normalize this
- var type = TmdbUtils.MapCrewToPersonType(person);
+ var crewMember = entry.CrewMember;
- if (!TmdbUtils.WantedCrewKinds.Contains(type)
- && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ 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;
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
index 22c065b5d..c671e7a93 100644
--- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs
index 71d60fc25..cd14764e4 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs
@@ -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.
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs
index bb66bddca..c20dfeeb5 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs
@@ -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,
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.Designer.cs
new file mode 100644
index 000000000..a0622c14d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.Designer.cs
@@ -0,0 +1,1709 @@
+// <auto-generated />
+using System;
+using Jellyfin.Database.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20250622170802_BaseItemImageInfoDateModifiedNullable")]
+ partial class BaseItemImageInfoDateModifiedNullable
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property<double>("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property<double>("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Album")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property<float?>("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property<string>("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Data")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ExtraIds")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingSubValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property<float?>("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property<string>("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property<string>("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000001"),
+ IsFolder = false,
+ IsInMixedFolder = false,
+ IsLocked = false,
+ IsMovie = false,
+ IsRepeat = false,
+ IsSeries = false,
+ IsVirtualItem = false,
+ Name = "This is a placeholder item for UserData that has been detacted from its original item",
+ Type = "PLACEHOLDER"
+ });
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<byte[]>("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property<DateTime?>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+ b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property<int>("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime>("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue");
+
+ b.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.Property<Guid>("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<long>("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamIndex");
+
+ b.HasIndex("StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType");
+
+ b.HasIndex("StreamIndex", "StreamType", "Language");
+
+ b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property<uint>("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<DateTime?>("RetentionDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.ToTable("UserData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Parents");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs
new file mode 100644
index 000000000..bce6029d5
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs
@@ -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);
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
index 706215eef..fe7870ed3 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
@@ -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")
diff --git a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs
index bd1b2b0da..87446236c 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs
@@ -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;
diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
index 74182171f..8ee129a57 100644
--- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
@@ -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);
}
diff --git a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
index d63ee6777..fcf37f35d 100644
--- a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs
@@ -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))
diff --git a/tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs b/tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs
index 756a688ab..a1fc067cc 100644
--- a/tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs
@@ -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);
}
}
diff --git a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
index b32ecf6ec..b5585f4fd 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
@@ -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 }
};
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
index b289c763b..6964c920b 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs
@@ -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