diff options
| author | JPVenson <github@jpb.email> | 2024-10-08 09:34:34 +0000 |
|---|---|---|
| committer | JPVenson <github@jpb.email> | 2024-10-08 09:34:34 +0000 |
| commit | d3a3d9fce3b891eb0be274a0cdc45a989e557652 (patch) | |
| tree | bd232ef477c259f1fafa204016f6efd4dcb8691f /Emby.Server.Implementations | |
| parent | ee1bdf4e222125ed7382165fd7e09599ca4bd4aa (diff) | |
| parent | aaf20592bb0bbdf4f0f0d99fed091758e68ae850 (diff) | |
Merge remote-tracking branch 'jellyfinorigin/master' into feature/EFUserData
Diffstat (limited to 'Emby.Server.Implementations')
41 files changed, 1541 insertions, 147 deletions
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index e86010513..91791a1c8 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -20,7 +20,9 @@ namespace Emby.Server.Implementations { PlaylistsAllowDuplicatesKey, bool.FalseString }, { BindToUnixSocketKey, bool.FalseString }, { SqliteCacheSizeKey, "20000" }, - { FfmpegSkipValidationKey, bool.FalseString } + { FfmpegSkipValidationKey, bool.FalseString }, + { FfmpegImgExtractPerfTradeoffKey, bool.FalseString }, + { DetectNetworkChangeKey, bool.TrueString } }; } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index c7a8421c6..a2aeaf0fc 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Diagnostics; @@ -212,6 +210,949 @@ namespace Emby.Server.Implementations.Data /// <inheritdoc /> protected override TempStoreMode TempStore => TempStoreMode.Memory; + /// <summary> + /// Opens the connection to the database. + /// </summary> + public override void Initialize() + { + base.Initialize(); + + const string CreateMediaStreamsTableCommand + = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))"; + + const string CreateMediaAttachmentsTableCommand + = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))"; + + string[] queries = + { + "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", + + "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))", + "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", + "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)", + + "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)", + + "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", + + "drop index if exists idxPeopleItemId", + "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)", + "create index if not exists idxPeopleName on People(Name)", + + "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", + + CreateMediaStreamsTableCommand, + CreateMediaAttachmentsTableCommand, + + "pragma shrink_memory" + }; + + string[] postQueries = + { + "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", + "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", + + "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)", + "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)", + "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", + + // covering index + "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)", + + // series + "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)", + + // series counts + // seriesdateplayed sort order + "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)", + + // live tv programs + "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", + + // covering index for getitemvalues + "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)", + + // used by movie suggestions + "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)", + "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)", + + // latest items + "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)", + "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)", + + // resume + "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)", + + // items by name + "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", + "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)", + + // Used to update inherited tags + "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)", + + "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)", + "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)" + }; + + using (var connection = GetConnection()) + using (var transaction = connection.BeginTransaction()) + { + connection.Execute(string.Join(';', queries)); + + var existingColumnNames = GetColumnNames(connection, "AncestorIds"); + AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); + + existingColumnNames = GetColumnNames(connection, "TypedBaseItems"); + + AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames); + AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); + + existingColumnNames = GetColumnNames(connection, "ItemValues"); + AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames); + + existingColumnNames = GetColumnNames(connection, ChaptersTableName); + AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); + + existingColumnNames = GetColumnNames(connection, "MediaStreams"); + AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames); + AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); + + AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); + + AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); + AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); + + AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); + + AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames); + + connection.Execute(string.Join(';', postQueries)); + + transaction.Commit(); + } + } + + /// <inheritdoc /> + public void SaveImages(BaseItem item) + { + ArgumentNullException.ThrowIfNull(item); + + CheckDisposed(); + + var images = SerializeImages(item.ImageInfos); + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id"); + saveImagesStatement.TryBind("@Id", item.Id); + saveImagesStatement.TryBind("@Images", images); + + saveImagesStatement.ExecuteNonQuery(); + transaction.Commit(); + } + + /// <summary> + /// Saves the items. + /// </summary> + /// <param name="items">The items.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>. + /// </exception> + public void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(items); + + cancellationToken.ThrowIfCancellationRequested(); + + CheckDisposed(); + + var itemsLen = items.Count; + var tuples = new ValueTuple<BaseItem, List<Guid>, BaseItem, string, List<string>>[itemsLen]; + for (int i = 0; i < itemsLen; i++) + { + var item = items[i]; + var ancestorIds = item.SupportsAncestors ? + item.GetAncestorIds().Distinct().ToList() : + null; + + var topParent = item.GetTopParent(); + + var userdataKey = item.GetUserDataKeys().FirstOrDefault(); + var inheritedTags = item.GetInheritedTags(); + + tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); + } + + using var connection = GetConnection(); + using var transaction = connection.BeginTransaction(); + SaveItemsInTransaction(connection, tuples); + transaction.Commit(); + } + + private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples) + { + using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText)) + using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) + { + var requiresReset = false; + foreach (var tuple in tuples) + { + if (requiresReset) + { + saveItemStatement.Parameters.Clear(); + deleteAncestorsStatement.Parameters.Clear(); + } + + var item = tuple.Item; + var topParent = tuple.TopParent; + var userDataKey = tuple.UserDataKey; + + SaveItem(item, topParent, userDataKey, saveItemStatement); + + var inheritedTags = tuple.InheritedTags; + + if (item.SupportsAncestors) + { + UpdateAncestors(item.Id, tuple.AncestorIds, db, deleteAncestorsStatement); + } + + UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db); + + requiresReset = true; + } + } + } + + private string GetPathToSave(string path) + { + if (path is null) + { + return null; + } + + return _appHost.ReverseVirtualPath(path); + } + + private string RestorePath(string path) + { + return _appHost.ExpandVirtualPath(path); + } + + private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement) + { + Type type = item.GetType(); + + saveItemStatement.TryBind("@guid", item.Id); + saveItemStatement.TryBind("@type", type.FullName); + + if (TypeRequiresDeserialization(type)) + { + saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true); + } + else + { + saveItemStatement.TryBindNull("@data"); + } + + saveItemStatement.TryBind("@Path", GetPathToSave(item.Path)); + + if (item is IHasStartDate hasStartDate) + { + saveItemStatement.TryBind("@StartDate", hasStartDate.StartDate); + } + else + { + saveItemStatement.TryBindNull("@StartDate"); + } + + if (item.EndDate.HasValue) + { + saveItemStatement.TryBind("@EndDate", item.EndDate.Value); + } + else + { + saveItemStatement.TryBindNull("@EndDate"); + } + + saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); + + if (item is IHasProgramAttributes hasProgramAttributes) + { + saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie); + saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries); + saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle); + saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat); + } + else + { + saveItemStatement.TryBindNull("@IsMovie"); + saveItemStatement.TryBindNull("@IsSeries"); + saveItemStatement.TryBindNull("@EpisodeTitle"); + saveItemStatement.TryBindNull("@IsRepeat"); + } + + saveItemStatement.TryBind("@CommunityRating", item.CommunityRating); + saveItemStatement.TryBind("@CustomRating", item.CustomRating); + saveItemStatement.TryBind("@IndexNumber", item.IndexNumber); + saveItemStatement.TryBind("@IsLocked", item.IsLocked); + saveItemStatement.TryBind("@Name", item.Name); + saveItemStatement.TryBind("@OfficialRating", item.OfficialRating); + saveItemStatement.TryBind("@MediaType", item.MediaType.ToString()); + saveItemStatement.TryBind("@Overview", item.Overview); + saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber); + saveItemStatement.TryBind("@PremiereDate", item.PremiereDate); + saveItemStatement.TryBind("@ProductionYear", item.ProductionYear); + + var parentId = item.ParentId; + if (parentId.IsEmpty()) + { + saveItemStatement.TryBindNull("@ParentId"); + } + else + { + saveItemStatement.TryBind("@ParentId", parentId); + } + + if (item.Genres.Length > 0) + { + saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres)); + } + else + { + saveItemStatement.TryBindNull("@Genres"); + } + + saveItemStatement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue); + + saveItemStatement.TryBind("@SortName", item.SortName); + + saveItemStatement.TryBind("@ForcedSortName", item.ForcedSortName); + + saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks); + saveItemStatement.TryBind("@Size", item.Size); + + saveItemStatement.TryBind("@DateCreated", item.DateCreated); + saveItemStatement.TryBind("@DateModified", item.DateModified); + + saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage); + saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode); + + if (item.Width > 0) + { + saveItemStatement.TryBind("@Width", item.Width); + } + else + { + saveItemStatement.TryBindNull("@Width"); + } + + if (item.Height > 0) + { + saveItemStatement.TryBind("@Height", item.Height); + } + else + { + saveItemStatement.TryBindNull("@Height"); + } + + if (item.DateLastRefreshed != default(DateTime)) + { + saveItemStatement.TryBind("@DateLastRefreshed", item.DateLastRefreshed); + } + else + { + saveItemStatement.TryBindNull("@DateLastRefreshed"); + } + + if (item.DateLastSaved != default(DateTime)) + { + saveItemStatement.TryBind("@DateLastSaved", item.DateLastSaved); + } + else + { + saveItemStatement.TryBindNull("@DateLastSaved"); + } + + saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder); + + if (item.LockedFields.Length > 0) + { + saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields)); + } + else + { + saveItemStatement.TryBindNull("@LockedFields"); + } + + if (item.Studios.Length > 0) + { + saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios)); + } + else + { + saveItemStatement.TryBindNull("@Studios"); + } + + if (item.Audio.HasValue) + { + saveItemStatement.TryBind("@Audio", item.Audio.Value.ToString()); + } + else + { + saveItemStatement.TryBindNull("@Audio"); + } + + if (item is LiveTvChannel liveTvChannel) + { + saveItemStatement.TryBind("@ExternalServiceId", liveTvChannel.ServiceName); + } + else + { + saveItemStatement.TryBindNull("@ExternalServiceId"); + } + + if (item.Tags.Length > 0) + { + saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags)); + } + else + { + saveItemStatement.TryBindNull("@Tags"); + } + + saveItemStatement.TryBind("@IsFolder", item.IsFolder); + + saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString()); + + if (topParent is null) + { + saveItemStatement.TryBindNull("@TopParentId"); + } + else + { + saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N", CultureInfo.InvariantCulture)); + } + + if (item is Trailer trailer && trailer.TrailerTypes.Length > 0) + { + saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes)); + } + else + { + saveItemStatement.TryBindNull("@TrailerTypes"); + } + + saveItemStatement.TryBind("@CriticRating", item.CriticRating); + + if (string.IsNullOrWhiteSpace(item.Name)) + { + saveItemStatement.TryBindNull("@CleanName"); + } + else + { + saveItemStatement.TryBind("@CleanName", GetCleanValue(item.Name)); + } + + saveItemStatement.TryBind("@PresentationUniqueKey", item.PresentationUniqueKey); + saveItemStatement.TryBind("@OriginalTitle", item.OriginalTitle); + + if (item is Video video) + { + saveItemStatement.TryBind("@PrimaryVersionId", video.PrimaryVersionId); + } + else + { + saveItemStatement.TryBindNull("@PrimaryVersionId"); + } + + if (item is Folder folder && folder.DateLastMediaAdded.HasValue) + { + saveItemStatement.TryBind("@DateLastMediaAdded", folder.DateLastMediaAdded.Value); + } + else + { + saveItemStatement.TryBindNull("@DateLastMediaAdded"); + } + + saveItemStatement.TryBind("@Album", item.Album); + saveItemStatement.TryBind("@LUFS", item.LUFS); + saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain); + saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem); + + if (item is IHasSeries hasSeriesName) + { + saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName); + } + else + { + saveItemStatement.TryBindNull("@SeriesName"); + } + + if (string.IsNullOrWhiteSpace(userDataKey)) + { + saveItemStatement.TryBindNull("@UserDataKey"); + } + else + { + saveItemStatement.TryBind("@UserDataKey", userDataKey); + } + + if (item is Episode episode) + { + saveItemStatement.TryBind("@SeasonName", episode.SeasonName); + + var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId; + + saveItemStatement.TryBind("@SeasonId", nullableSeasonId); + } + else + { + saveItemStatement.TryBindNull("@SeasonName"); + saveItemStatement.TryBindNull("@SeasonId"); + } + + if (item is IHasSeries hasSeries) + { + var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId; + + saveItemStatement.TryBind("@SeriesId", nullableSeriesId); + saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey); + } + else + { + saveItemStatement.TryBindNull("@SeriesId"); + saveItemStatement.TryBindNull("@SeriesPresentationUniqueKey"); + } + + saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId); + saveItemStatement.TryBind("@Tagline", item.Tagline); + + saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds)); + saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); + + if (item.ProductionLocations.Length > 0) + { + saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations)); + } + else + { + saveItemStatement.TryBindNull("@ProductionLocations"); + } + + if (item.ExtraIds.Length > 0) + { + saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds)); + } + else + { + saveItemStatement.TryBindNull("@ExtraIds"); + } + + saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate); + if (item.ExtraType.HasValue) + { + saveItemStatement.TryBind("@ExtraType", item.ExtraType.Value.ToString()); + } + else + { + saveItemStatement.TryBindNull("@ExtraType"); + } + + string artists = null; + if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0) + { + artists = string.Join('|', hasArtists.Artists); + } + + saveItemStatement.TryBind("@Artists", artists); + + string albumArtists = null; + if (item is IHasAlbumArtist hasAlbumArtists + && hasAlbumArtists.AlbumArtists.Count > 0) + { + albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists); + } + + saveItemStatement.TryBind("@AlbumArtists", albumArtists); + saveItemStatement.TryBind("@ExternalId", item.ExternalId); + + if (item is LiveTvProgram program) + { + saveItemStatement.TryBind("@ShowId", program.ShowId); + } + else + { + saveItemStatement.TryBindNull("@ShowId"); + } + + Guid ownerId = item.OwnerId; + if (ownerId.IsEmpty()) + { + saveItemStatement.TryBindNull("@OwnerId"); + } + else + { + saveItemStatement.TryBind("@OwnerId", ownerId); + } + + saveItemStatement.ExecuteNonQuery(); + } + + internal static string SerializeProviderIds(Dictionary<string, string> providerIds) + { + StringBuilder str = new StringBuilder(); + foreach (var i in providerIds) + { + // Ideally we shouldn't need this IsNullOrWhiteSpace check, + // but we're seeing some cases of bad data slip through + if (string.IsNullOrWhiteSpace(i.Value)) + { + continue; + } + + str.Append(i.Key) + .Append('=') + .Append(i.Value) + .Append('|'); + } + + if (str.Length == 0) + { + return null; + } + + str.Length -= 1; // Remove last | + return str.ToString(); + } + + internal static void DeserializeProviderIds(string value, IHasProviderIds item) + { + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + + foreach (var part in value.SpanSplit('|')) + { + var providerDelimiterIndex = part.IndexOf('='); + // Don't let empty values through + if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1) + { + item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString()); + } + } + } + + internal string SerializeImages(ItemImageInfo[] images) + { + if (images.Length == 0) + { + return null; + } + + StringBuilder str = new StringBuilder(); + foreach (var i in images) + { + if (string.IsNullOrWhiteSpace(i.Path)) + { + continue; + } + + AppendItemImageInfo(str, i); + str.Append('|'); + } + + str.Length -= 1; // Remove last | + return str.ToString(); + } + + internal ItemImageInfo[] DeserializeImages(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return Array.Empty<ItemImageInfo>(); + } + + // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed + var valueSpan = value.AsSpan(); + var count = valueSpan.Count('|') + 1; + + var position = 0; + var result = new ItemImageInfo[count]; + foreach (var part in valueSpan.Split('|')) + { + var image = ItemImageInfoFromValueString(part); + + if (image is not null) + { + result[position++] = image; + } + } + + if (position == count) + { + return result; + } + + if (position == 0) + { + return Array.Empty<ItemImageInfo>(); + } + + // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array. + return result[..position]; + } + + private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) + { + const char Delimiter = '*'; + + var path = image.Path ?? string.Empty; + + bldr.Append(GetPathToSave(path)) + .Append(Delimiter) + .Append(image.DateModified.Ticks) + .Append(Delimiter) + .Append(image.Type) + .Append(Delimiter) + .Append(image.Width) + .Append(Delimiter) + .Append(image.Height); + + var hash = image.BlurHash; + if (!string.IsNullOrEmpty(hash)) + { + bldr.Append(Delimiter) + // Replace delimiters with other characters. + // This can be removed when we migrate to a proper DB. + .Append(hash.Replace(Delimiter, '/').Replace('|', '\\')); + } + } + + internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan<char> value) + { + const char Delimiter = '*'; + + var nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + return null; + } + + ReadOnlySpan<char> path = value[..nextSegment]; + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + return null; + } + + ReadOnlySpan<char> dateModified = value[..nextSegment]; + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + nextSegment = value.Length; + } + + ReadOnlySpan<char> imageType = value[..nextSegment]; + + var image = new ItemImageInfo + { + Path = RestorePath(path.ToString()) + }; + + if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks) + && ticks >= DateTime.MinValue.Ticks + && ticks <= DateTime.MaxValue.Ticks) + { + image.DateModified = new DateTime(ticks, DateTimeKind.Utc); + } + else + { + return null; + } + + if (Enum.TryParse(imageType, true, out ImageType type)) + { + image.Type = type; + } + else + { + return null; + } + + // Optional parameters: width*height*blurhash + if (nextSegment + 1 < value.Length - 1) + { + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1 || nextSegment == value.Length) + { + return image; + } + + ReadOnlySpan<char> widthSpan = value[..nextSegment]; + + value = value[(nextSegment + 1)..]; + nextSegment = value.IndexOf(Delimiter); + if (nextSegment == -1) + { + nextSegment = value.Length; + } + + ReadOnlySpan<char> heightSpan = value[..nextSegment]; + + if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) + && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) + { + image.Width = width; + image.Height = height; + } + + if (nextSegment < value.Length - 1) + { + value = value[(nextSegment + 1)..]; + var length = value.Length; + + Span<char> blurHashSpan = stackalloc char[length]; + for (int i = 0; i < length; i++) + { + var c = value[i]; + blurHashSpan[i] = c switch + { + '/' => Delimiter, + '\\' => '|', + _ => c + }; + } + + image.BlurHash = new string(blurHashSpan); + } + } + + return image; + } + + /// <summary> + /// Internal retrieve from items or users table. + /// </summary> + /// <param name="id">The id.</param> + /// <returns>BaseItem.</returns> + /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception> + /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception> + public BaseItem RetrieveItem(Guid id) + { + if (id.IsEmpty()) + { + throw new ArgumentException("Guid can't be empty", nameof(id)); + } + + CheckDisposed(); + + using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) + { + statement.TryBind("@guid", id); + + foreach (var row in statement.ExecuteQuery()) + { + return GetItem(row, new InternalItemsQuery()); + } + } + + return null; + } + private bool TypeRequiresDeserialization(Type type) { if (_config.Configuration.SkipDeserializationForBasicTypes) @@ -694,9 +1635,6 @@ namespace Emby.Server.Implementations.Data if (query.SearchTerm.Length > 1) { builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)"); - builder.Append("+ (SELECT COUNT(1) * 1 from ItemValues where ItemId=Guid and CleanValue like @SearchTermContains)"); - builder.Append("+ (SELECT COUNT(1) * 2 from ItemValues where ItemId=Guid and CleanValue like @SearchTermStartsWith)"); - builder.Append("+ (SELECT COUNT(1) * 10 from ItemValues where ItemId=Guid and CleanValue like @SearchTermEquals)"); } builder.Append(") as SearchScore"); @@ -727,11 +1665,6 @@ namespace Emby.Server.Implementations.Data { statement.TryBind("@SearchTermContains", "%" + searchTerm + "%"); } - - if (commandText.Contains("@SearchTermEquals", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@SearchTermEquals", searchTerm); - } } private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement) @@ -797,6 +1730,7 @@ namespace Emby.Server.Implementations.Data return string.Empty; } + /// <inheritdoc /> public int GetCount(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -844,6 +1778,7 @@ namespace Emby.Server.Implementations.Data } } + /// <inheritdoc /> public List<BaseItem> GetItemList(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -997,6 +1932,7 @@ namespace Emby.Server.Implementations.Data items.Add(newItem); } + /// <inheritdoc /> public QueryResult<BaseItem> GetItems(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -1241,6 +2177,7 @@ namespace Emby.Server.Implementations.Data }; } + /// <inheritdoc /> public List<Guid> GetItemIdsList(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -2557,6 +3494,15 @@ namespace Emby.Server.Implementations.Data OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null) """); } + + // A playlist should be accessible to its owner regardless of allowed tags. + else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) + { + whereClauses.Add($""" + ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null + OR data like @PlaylistOwnerUserId) + """); + } else { whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)"); @@ -2568,6 +3514,11 @@ namespace Emby.Server.Implementations.Data { statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index])); } + + if (query.User is not null) + { + statement.TryBind("@PlaylistOwnerUserId", $"""%"OwnerUserId":"{query.User.Id.ToString("N")}"%"""); + } } } @@ -2785,6 +3736,7 @@ namespace Emby.Server.Implementations.Data || query.IncludeItemTypes.Contains(BaseItemKind.Season); } + /// <inheritdoc /> public void UpdateInheritedValues() { const string Statements = """ @@ -2801,6 +3753,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type transaction.Commit(); } + /// <inheritdoc /> public void DeleteItem(Guid id) { if (id.IsEmpty()) @@ -2843,6 +3796,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } } + /// <inheritdoc /> public List<string> GetPeopleNames(InternalPeopleQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -2881,6 +3835,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type return list; } + /// <inheritdoc /> public List<PersonInfo> GetPeople(InternalPeopleQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -3040,46 +3995,55 @@ AND Type = @InternalPersonType)"); } } + /// <inheritdoc /> public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query) { return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName); } + /// <inheritdoc /> public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query) { return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName); } + /// <inheritdoc /> public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query) { return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName); } + /// <inheritdoc /> public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query) { return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName); } + /// <inheritdoc /> public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query) { return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName); } + /// <inheritdoc /> public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query) { return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName); } + /// <inheritdoc /> public List<string> GetStudioNames() { return GetItemValueNames(new[] { 3 }, Array.Empty<string>(), Array.Empty<string>()); } + /// <inheritdoc /> public List<string> GetAllArtistNames() { return GetItemValueNames(new[] { 0, 1 }, Array.Empty<string>(), Array.Empty<string>()); } + /// <inheritdoc /> public List<string> GetMusicGenreNames() { return GetItemValueNames( @@ -3094,6 +4058,7 @@ AND Type = @InternalPersonType)"); Array.Empty<string>()); } + /// <inheritdoc /> public List<string> GetGenreNames() { return GetItemValueNames( @@ -3571,6 +4536,7 @@ AND Type = @InternalPersonType)"); } } + /// <inheritdoc /> public void UpdatePeople(Guid itemId, List<PersonInfo> people) { if (itemId.IsEmpty()) @@ -3672,6 +4638,7 @@ AND Type = @InternalPersonType)"); return item; } + /// <inheritdoc /> public List<MediaStream> GetMediaStreams(MediaStreamQuery query) { CheckDisposed(); @@ -3720,6 +4687,7 @@ AND Type = @InternalPersonType)"); } } + /// <inheritdoc /> public void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken) { CheckDisposed(); @@ -4074,6 +5042,7 @@ AND Type = @InternalPersonType)"); return item; } + /// <inheritdoc /> public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query) { CheckDisposed(); @@ -4109,6 +5078,7 @@ AND Type = @InternalPersonType)"); return list; } + /// <inheritdoc /> public void SaveMediaAttachments( Guid id, IReadOnlyList<MediaAttachment> attachments, diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 31617d1a5..6af2a553d 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -314,6 +314,12 @@ namespace Emby.Server.Implementations.IO var ex = e.GetException(); var dw = (FileSystemWatcher)sender; + if (ex is UnauthorizedAccessException unauthorizedAccessException) + { + _logger.LogError(unauthorizedAccessException, "Permission error for Directory watcher: {Path}", dw.Path); + return; + } + _logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path); DisposeWatcher(dw, true); diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 28bb29df8..4b68f21d5 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -149,6 +149,26 @@ namespace Emby.Server.Implementations.IO } } + /// <inheritdoc /> + public void MoveDirectory(string source, string destination) + { + try + { + Directory.Move(source, destination); + } + catch (IOException) + { + // Cross device move requires a copy + Directory.CreateDirectory(destination); + foreach (string file in Directory.GetFiles(source)) + { + File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), true); + } + + Directory.Delete(source, true); + } + } + /// <summary> /// Returns a <see cref="FileSystemMetadata"/> object for the specified file or directory path. /// </summary> @@ -327,11 +347,7 @@ namespace Emby.Server.Implementations.IO } } - /// <summary> - /// Gets the creation time UTC. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>DateTime.</returns> + /// <inheritdoc /> public virtual DateTime GetCreationTimeUtc(string path) { return GetCreationTimeUtc(GetFileSystemInfo(path)); @@ -368,11 +384,7 @@ namespace Emby.Server.Implementations.IO } } - /// <summary> - /// Gets the last write time UTC. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>DateTime.</returns> + /// <inheritdoc /> public virtual DateTime GetLastWriteTimeUtc(string path) { return GetLastWriteTimeUtc(GetFileSystemInfo(path)); @@ -446,11 +458,7 @@ namespace Emby.Server.Implementations.IO File.SetAttributes(path, attributes); } - /// <summary> - /// Swaps the files. - /// </summary> - /// <param name="file1">The file1.</param> - /// <param name="file2">The file2.</param> + /// <inheritdoc /> public virtual void SwapFiles(string file1, string file2) { ArgumentException.ThrowIfNullOrEmpty(file1); diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index a2301c8ae..bb45dd87e 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -50,6 +50,10 @@ namespace Emby.Server.Implementations.Library "**/lost+found/**", "**/lost+found", + // Trickplay files + "**/*.trickplay", + "**/*.trickplay/**", + // WMC temp recording directories that will constantly be written to "**/TempRec/**", "**/TempRec", diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 48d24385e..28f7ed659 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2725,33 +2725,9 @@ namespace Emby.Server.Implementations.Library public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem) { - string? newPath; - if (ownerItem is not null) - { - var libraryOptions = GetLibraryOptions(ownerItem); - if (libraryOptions is not null) - { - foreach (var pathInfo in libraryOptions.PathInfos) - { - if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath)) - { - return newPath; - } - } - } - } - - var metadataPath = _configurationManager.Configuration.MetadataPath; - var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath; - - if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath)) - { - return newPath; - } - foreach (var map in _configurationManager.Configuration.PathSubstitutions) { - if (path.TryReplaceSubPath(map.From, map.To, out newPath)) + if (path.TryReplaceSubPath(map.From, map.To, out var newPath)) { return newPath; } @@ -3070,15 +3046,6 @@ namespace Emby.Server.Implementations.Library SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions); - foreach (var originalPathInfo in libraryOptions.PathInfos) - { - if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal)) - { - originalPathInfo.NetworkPath = mediaPath.NetworkPath; - break; - } - } - CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions); } diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 9172af516..97aa0ca58 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -129,5 +129,11 @@ "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання", "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.", "TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.", - "TaskAudioNormalization": "Нармалізацыя гуку" + "TaskAudioNormalization": "Нармалізацыя гуку", + "TaskExtractMediaSegmentsDescription": "Выдае або атрымлівае медыясегменты з убудоў з падтрымкай MediaSegment.", + "TaskMoveTrickplayImagesDescription": "Перамяшчае існуючыя файлы trickplay у адпаведнасці з наладамі бібліятэкі.", + "TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень", + "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень", + "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", + "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay" } diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 2998489b5..6b3b78fa1 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -130,5 +130,7 @@ "TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.", "TaskCleanCollectionsAndPlaylists": "Neteja col·leccions i llistes de reproducció", "TaskAudioNormalization": "Normalització d'Àudio", - "TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio." + "TaskAudioNormalizationDescription": "Escaneja arxius per dades de normalització d'àudio.", + "TaskDownloadMissingLyricsDescription": "Baixar lletres de les cançons", + "TaskDownloadMissingLyrics": "Baixar lletres que falten" } diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index ad9e555a3..ba2e2700d 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Normalizace zvuku", "TaskAudioNormalizationDescription": "Skenovat soubory za účelem normalizace zvuku.", "TaskDownloadMissingLyrics": "Stáhnout chybějící texty k písni", - "TaskDownloadMissingLyricsDescription": "Stáhne texty k písni" + "TaskDownloadMissingLyricsDescription": "Stáhne texty k písni", + "TaskExtractMediaSegments": "Skenování segmentů médií", + "TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.", + "TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay", + "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny." } diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index bbb162c77..51c9e87d5 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Audio Normalisierung", "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.", "TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter", - "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen" + "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen", + "TaskExtractMediaSegments": "Scanne Mediensegmente", + "TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.", + "TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren", + "TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben." } diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 65df1e45b..ca52ffb14 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Audio Normalisation", "TaskAudioNormalizationDescription": "Scans files for audio normalisation data.", "TaskDownloadMissingLyrics": "Download missing lyrics", - "TaskDownloadMissingLyricsDescription": "Downloads lyrics for songs" + "TaskDownloadMissingLyricsDescription": "Downloads lyrics for songs", + "TaskExtractMediaSegments": "Media Segment Scan", + "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.", + "TaskMoveTrickplayImages": "Migrate Trickplay Image Location", + "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings." } diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index d1410ef5e..9702ab712 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -131,5 +131,9 @@ "TaskKeyframeExtractor": "Keyframe Extractor", "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.", "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists", - "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist." + "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.", + "TaskExtractMediaSegments": "Media Segment Scan", + "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.", + "TaskMoveTrickplayImages": "Migrate Trickplay Image Location", + "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings." } diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index b926d9d30..f2f657b04 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -132,5 +132,9 @@ "TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción", "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.", "TaskDownloadMissingLyrics": "Descargar letra faltante", - "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones" + "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones", + "TaskExtractMediaSegments": "Escanear Segmentos de Media", + "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medio de plugins habilitados para MediaSegment.", + "TaskMoveTrickplayImagesDescription": "Mueve archivos existentes de trickplay de acuerdo a la configuración de la biblioteca.", + "TaskMoveTrickplayImages": "Migrar Ubicación de Imagen de Trickplay" } diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index 075bcc9a4..3b2bb70a9 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -102,7 +102,7 @@ "Forced": "Sunnitud", "Folders": "Kaustad", "Favorites": "Lemmikud", - "FailedLoginAttemptWithUserName": "{0} - sisselogimine nurjus", + "FailedLoginAttemptWithUserName": "Sisselogimine nurjus aadressilt {0}", "DeviceOnlineWithName": "{0} on ühendatud", "DeviceOfflineWithName": "{0} katkestas ühenduse", "Default": "Vaikimisi", @@ -129,5 +129,11 @@ "TaskAudioNormalization": "Heli Normaliseerimine", "TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks.", "TaskCleanCollectionsAndPlaylistsDescription": "Eemaldab kogumikest ja esitusloenditest asjad, mida enam ei eksisteeri.", - "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid" + "TaskCleanCollectionsAndPlaylists": "Puhasta kogumikud ja esitusloendid", + "TaskDownloadMissingLyrics": "Lae alla puuduolev lüürika", + "TaskDownloadMissingLyricsDescription": "Lae lauludele alla lüürika", + "TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.", + "TaskExtractMediaSegments": "Meediasegmentide skaneerimine", + "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.", + "TaskMoveTrickplayImages": "Migreeri trickplay piltide asukoht" } diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index b0ddec104..ff14c1367 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -132,5 +132,9 @@ "TaskAudioNormalizationDescription": "بررسی فایل برای دادههای نرمال کردن صدا.", "TaskDownloadMissingLyrics": "دانلود متنهای ناموجود", "TaskDownloadMissingLyricsDescription": "دانلود متن شعرها", - "TaskAudioNormalization": "نرمال کردن صدا" + "TaskAudioNormalization": "نرمال کردن صدا", + "TaskExtractMediaSegments": "بررسی بخش محتوا", + "TaskExtractMediaSegmentsDescription": "بخشهای محتوا را از افزونههای مربوط استخراح میکند.", + "TaskMoveTrickplayImages": "جابهجایی عکسهای Trickplay", + "TaskMoveTrickplayImagesDescription": "دادههای Trickplay را با توجه به تنظیمات کتابخانه جابهجا میکند." } diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index dced61c5e..8a88cf28e 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -129,5 +129,6 @@ "TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.", "TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat", "TaskAudioNormalization": "Äänenvoimakkuuden normalisointi", - "TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja." + "TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja.", + "TaskDownloadMissingLyrics": "Lataa puuttuva lyriikka" } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 1dba78add..3caf8b547 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Normalisation audio", "TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio.", "TaskDownloadMissingLyricsDescription": "Téléchargement des paroles des chansons", - "TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes" + "TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes", + "TaskExtractMediaSegments": "Analyse des segments de média", + "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay", + "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.", + "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque." } diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json index 76a98aa54..3ba3e6679 100644 --- a/Emby.Server.Implementations/Localization/Core/gl.json +++ b/Emby.Server.Implementations/Localization/Core/gl.json @@ -1,7 +1,7 @@ { "Albums": "Álbumes", - "Collections": "Colecións", - "ChapterNameValue": "Capítulos {0}", + "Collections": "Coleccións", + "ChapterNameValue": "Capítulo {0}", "Channels": "Canles", "CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}", "Books": "Libros", diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index c8e036424..af57b1693 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -60,7 +60,7 @@ "NotificationOptionUserLockedOut": "משתמש ננעל", "NotificationOptionVideoPlayback": "ניגון וידאו החל", "NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק", - "Photos": "תמונות", + "Photos": "צילומים", "Playlists": "רשימות נגינה", "Plugin": "תוסף", "PluginInstalledWithName": "{0} הותקן", @@ -130,5 +130,11 @@ "TaskAudioNormalization": "נרמול שמע", "TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.", "TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.", - "TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה" + "TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה", + "TaskDownloadMissingLyrics": "הורדת מילים חסרות", + "TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים", + "TaskMoveTrickplayImages": "מעביר את מיקום תמונות Trickplay", + "TaskExtractMediaSegments": "סריקת מדיה", + "TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.", + "TaskMoveTrickplayImagesDescription": "מזיז קבצי trickplay קיימים בהתאם להגדרות הספרייה." } diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 31d6aaedb..f205e8b64 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -1,13 +1,13 @@ { "Albums": "Albumok", - "AppDeviceValues": "Program: {0}, Eszköz: {1}", + "AppDeviceValues": "Program: {0}, eszköz: {1}", "Application": "Alkalmazás", "Artists": "Előadók", "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve", "Books": "Könyvek", "CameraImageUploadedFrom": "Új kamerakép lett feltöltve innen: {0}", "Channels": "Csatornák", - "ChapterNameValue": "Jelenet {0}", + "ChapterNameValue": "{0}. jelenet", "Collections": "Gyűjtemények", "DeviceOfflineWithName": "{0} kijelentkezett", "DeviceOnlineWithName": "{0} belépett", @@ -15,31 +15,31 @@ "Favorites": "Kedvencek", "Folders": "Könyvtárak", "Genres": "Műfajok", - "HeaderAlbumArtists": "Album előadók", + "HeaderAlbumArtists": "Albumelőadók", "HeaderContinueWatching": "Megtekintés folytatása", - "HeaderFavoriteAlbums": "Kedvenc Albumok", - "HeaderFavoriteArtists": "Kedvenc Előadók", - "HeaderFavoriteEpisodes": "Kedvenc Epizódok", - "HeaderFavoriteShows": "Kedvenc Sorozatok", - "HeaderFavoriteSongs": "Kedvenc Dalok", + "HeaderFavoriteAlbums": "Kedvenc albumok", + "HeaderFavoriteArtists": "Kedvenc előadók", + "HeaderFavoriteEpisodes": "Kedvenc epizódok", + "HeaderFavoriteShows": "Kedvenc sorozatok", + "HeaderFavoriteSongs": "Kedvenc számok", "HeaderLiveTV": "Élő TV", "HeaderNextUp": "Következik", - "HeaderRecordingGroups": "Felvevő Csoportok", - "HomeVideos": "Otthoni Videók", - "Inherit": "Örökölt", - "ItemAddedWithName": "{0} hozzáadva a könyvtárhoz", - "ItemRemovedWithName": "{0} eltávolítva a könyvtárból", + "HeaderRecordingGroups": "Felvételi csoportok", + "HomeVideos": "Otthoni videók", + "Inherit": "Öröklés", + "ItemAddedWithName": "{0} hozzáadva a médiatárhoz", + "ItemRemovedWithName": "{0} eltávolítva a médiatárból", "LabelIpAddressValue": "IP-cím: {0}", "LabelRunningTimeValue": "Lejátszási idő: {0}", "Latest": "Legújabb", "MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve lett", "MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}", "MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve lett: {0}", - "MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve lett", + "MessageServerConfigurationUpdated": "A kiszolgálókonfiguráció frissítve lett", "MixedContent": "Vegyes tartalom", "Movies": "Filmek", "Music": "Zenék", - "MusicVideos": "Zenei videóklippek", + "MusicVideos": "Zenei videóklipek", "NameInstallFailed": "{0} sikertelen telepítés", "NameSeasonNumber": "{0}. évad", "NameSeasonUnknown": "Ismeretlen évad", @@ -56,7 +56,7 @@ "NotificationOptionPluginUninstalled": "Bővítmény eltávolítva", "NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve", "NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges", - "NotificationOptionTaskFailed": "Ütemezett feladat hiba", + "NotificationOptionTaskFailed": "Hiba az ütemezett feladatban", "NotificationOptionUserLockedOut": "Felhasználó tiltva", "NotificationOptionVideoPlayback": "Videólejátszás elkezdve", "NotificationOptionVideoPlaybackStopped": "Videólejátszás leállítva", @@ -107,7 +107,7 @@ "TaskCleanCache": "Gyorsítótár könyvtárának ürítése", "TasksChannelsCategory": "Internetes csatornák", "TasksApplicationCategory": "Alkalmazás", - "TasksLibraryCategory": "Könyvtár", + "TasksLibraryCategory": "Médiatár", "TasksMaintenanceCategory": "Karbantartás", "TaskDownloadMissingSubtitlesDescription": "A metaadat-konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.", "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése", @@ -119,16 +119,22 @@ "Undefined": "Meghatározatlan", "Forced": "Kényszerített", "Default": "Alapértelmezett", - "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.", + "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a médiatár beolvasása, vagy egyéb adatbázis-módosítást igénylő változtatás végrehajtása után, javíthatja a teljesítményt.", "TaskOptimizeDatabase": "Adatbázis optimalizálása", "TaskKeyframeExtractor": "Kulcsképkockák kibontása", "TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.", "External": "Külső", "HearingImpaired": "Hallássérült", - "TaskRefreshTrickplayImages": "Trickplay képek generálása", + "TaskRefreshTrickplayImages": "Trickplay képek előállítása", "TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.", - "TaskAudioNormalization": "Hangerő Normalizáció", + "TaskAudioNormalization": "Hangerő-normalizálás", "TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.", - "TaskAudioNormalizationDescription": "Hangerő normalizációs adatok keresése.", - "TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása" + "TaskAudioNormalizationDescription": "Hangerő-normalizálási adatok keresése.", + "TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása", + "TaskExtractMediaSegments": "Médiaszegmens felismerése", + "TaskDownloadMissingLyrics": "Hiányzó szöveg letöltése", + "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése", + "TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése", + "TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.", + "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből." } diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 961d1a0df..6b0cfb359 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -132,5 +132,7 @@ "TaskAudioNormalization": "Normalizzazione dell'audio", "TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.", "TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni", - "TaskDownloadMissingLyrics": "Scarica testi mancanti" + "TaskDownloadMissingLyrics": "Scarica testi mancanti", + "TaskMoveTrickplayImages": "Sposta le immagini Trickplay", + "TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria." } diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index c8ed7d0fb..10f4aee25 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -129,5 +129,10 @@ "TaskCleanCollectionsAndPlaylists": "コレクションとプレイリストをクリーンアップ", "TaskAudioNormalization": "音声の正規化", "TaskAudioNormalizationDescription": "音声の正規化データのためにファイルをスキャンします。", - "TaskCleanCollectionsAndPlaylistsDescription": "在しなくなったコレクションやプレイリストからアイテムを削除します。" + "TaskCleanCollectionsAndPlaylistsDescription": "在しなくなったコレクションやプレイリストからアイテムを削除します。", + "TaskDownloadMissingLyricsDescription": "歌詞をダウンロード", + "TaskExtractMediaSegments": "メディアセグメントを読み取る", + "TaskMoveTrickplayImages": "Trickplayの画像を移動", + "TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。", + "TaskDownloadMissingLyrics": "記録されていない歌詞をダウンロード" } diff --git a/Emby.Server.Implementations/Localization/Core/kw.json b/Emby.Server.Implementations/Localization/Core/kw.json index ffb4345c8..336d286fc 100644 --- a/Emby.Server.Implementations/Localization/Core/kw.json +++ b/Emby.Server.Implementations/Localization/Core/kw.json @@ -131,5 +131,9 @@ "TaskCleanCollectionsAndPlaylists": "Glanhe kuntellow ha rolyow-gwari", "TaskKeyframeExtractor": "Estennell Framalhwedh", "TaskCleanCollectionsAndPlaylistsDescription": "Y hwra dilea taklow a-dhyworth kuntellow ha rolyow-gwari na vos na moy.", - "TaskKeyframeExtractorDescription": "Y hwra kuntel framyowalhwedh a-dhyworth restrennow gwydhyowyow rag gul rolyow-gwari HLS moy poran. Martesen y hwra an oberen ma ow ponya rag termyn hir." + "TaskKeyframeExtractorDescription": "Y hwra kuntel framyowalhwedh a-dhyworth restrennow gwydhyowyow rag gul rolyow-gwari HLS moy poran. Martesen y hwra an oberen ma ow ponya rag termyn hir.", + "TaskExtractMediaSegments": "Arhwilas Rann Media", + "TaskExtractMediaSegmentsDescription": "Kavos rannow media a-dhyworth ystynansow gallosegys MediaSegment.", + "TaskMoveTrickplayImages": "Divroa Tyller Imach TrickPlay", + "TaskMoveTrickplayImagesDescription": "Y hwra movya restrennow a-lemmyn trickplay herwydh settyansow lyverva." } diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json index 7ef907918..e149f8adf 100644 --- a/Emby.Server.Implementations/Localization/Core/mk.json +++ b/Emby.Server.Implementations/Localization/Core/mk.json @@ -55,7 +55,7 @@ "Genres": "Жанрови", "Folders": "Папки", "Favorites": "Омилени", - "FailedLoginAttemptWithUserName": "Неуспешно поврзување од {0}", + "FailedLoginAttemptWithUserName": "Неуспешен обид за најавување од {0}", "DeviceOnlineWithName": "{0} е приклучен", "DeviceOfflineWithName": "{0} се исклучи", "Collections": "Колекции", @@ -123,5 +123,13 @@ "TaskCleanActivityLogDescription": "Избришува логови на активности постари од определеното време.", "TaskCleanActivityLog": "Избриши Лог на Активности", "External": "Надворешен", - "HearingImpaired": "Оштетен слух" + "HearingImpaired": "Оштетен слух", + "TaskCleanCollectionsAndPlaylists": "Исчисти ги колекциите и плејлистите", + "TaskAudioNormalizationDescription": "Скенирање датотеки за податоци за нормализација на звукот.", + "TaskDownloadMissingLyrics": "Преземи стихови кои недостасуваат", + "TaskDownloadMissingLyricsDescription": "Преземи стихови/текстови за песни", + "TaskRefreshTrickplayImages": "Генерирај слики за прегледување (Trickplay)", + "TaskAudioNormalization": "Нормализација на звукот", + "TaskRefreshTrickplayImagesDescription": "Креира трикплеј прегледи за видеа во овозможените библиотеки.", + "TaskCleanCollectionsAndPlaylistsDescription": "Отстранува ставки од колекциите и плејлистите што веќе не постојат." } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 747652538..b1b6e96ea 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -128,9 +128,13 @@ "TaskRefreshTrickplayImages": "Generer Trickplay bilder", "TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker.", "TaskCleanCollectionsAndPlaylists": "Rydd kolleksjoner og spillelister", - "TaskAudioNormalization": "Lyd Normalisering", - "TaskAudioNormalizationDescription": "Skan filer for lyd normaliserende data", - "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes", + "TaskAudioNormalization": "Lydnormalisering", + "TaskAudioNormalizationDescription": "Skan filer for lydnormaliserende data.", + "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes.", "TaskDownloadMissingLyrics": "Last ned manglende tekster", - "TaskDownloadMissingLyricsDescription": "Last ned sangtekster" + "TaskDownloadMissingLyricsDescription": "Last ned sangtekster", + "TaskExtractMediaSegments": "Skann mediasegment", + "TaskMoveTrickplayImages": "Migrer bildeplassering for Trickplay", + "TaskMoveTrickplayImagesDescription": "Flytter eksisterende Trickplay-filer i henhold til bibliotekseinstillingene.", + "TaskExtractMediaSegmentsDescription": "Trekker ut eller henter mediasegmenter fra plugins som støtter MediaSegment." } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 39e7cd546..7d101195b 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -16,13 +16,13 @@ "Folders": "Mappen", "Genres": "Genres", "HeaderAlbumArtists": "Albumartiesten", - "HeaderContinueWatching": "Kijken hervatten", + "HeaderContinueWatching": "Verderkijken", "HeaderFavoriteAlbums": "Favoriete albums", "HeaderFavoriteArtists": "Favoriete artiesten", "HeaderFavoriteEpisodes": "Favoriete afleveringen", - "HeaderFavoriteShows": "Favoriete shows", + "HeaderFavoriteShows": "Favoriete series", "HeaderFavoriteSongs": "Favoriete nummers", - "HeaderLiveTV": "Live TV", + "HeaderLiveTV": "Live-tv", "HeaderNextUp": "Volgende", "HeaderRecordingGroups": "Opnamegroepen", "HomeVideos": "Homevideo's", @@ -34,8 +34,8 @@ "Latest": "Nieuwste", "MessageApplicationUpdated": "Jellyfin Server is bijgewerkt", "MessageApplicationUpdatedTo": "Jellyfin Server is bijgewerkt naar {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Sectie {0} van de server configuratie is bijgewerkt", - "MessageServerConfigurationUpdated": "Server configuratie is bijgewerkt", + "MessageNamedServerConfigurationUpdatedWithValue": "Sectie {0} van de serverconfiguratie is bijgewerkt", + "MessageServerConfigurationUpdated": "Serverconfiguratie is bijgewerkt", "MixedContent": "Gemengde inhoud", "Movies": "Films", "Music": "Muziek", @@ -50,12 +50,12 @@ "NotificationOptionAudioPlaybackStopped": "Muziek gestopt", "NotificationOptionCameraImageUploaded": "Camera-afbeelding geüpload", "NotificationOptionInstallationFailed": "Installatie mislukt", - "NotificationOptionNewLibraryContent": "Nieuwe content toegevoegd", - "NotificationOptionPluginError": "Plug-in fout", + "NotificationOptionNewLibraryContent": "Nieuwe inhoud toegevoegd", + "NotificationOptionPluginError": "Plug-in-fout", "NotificationOptionPluginInstalled": "Plug-in geïnstalleerd", "NotificationOptionPluginUninstalled": "Plug-in verwijderd", "NotificationOptionPluginUpdateInstalled": "Plug-in-update geïnstalleerd", - "NotificationOptionServerRestartRequired": "Server herstart nodig", + "NotificationOptionServerRestartRequired": "Herstarten server vereist", "NotificationOptionTaskFailed": "Geplande taak mislukt", "NotificationOptionUserLockedOut": "Gebruiker is vergrendeld", "NotificationOptionVideoPlayback": "Afspelen van video gestart", @@ -72,16 +72,16 @@ "ServerNameNeedsToBeRestarted": "{0} moet herstart worden", "Shows": "Series", "Songs": "Nummers", - "StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden, probeer het later opnieuw.", + "StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden. Probeer het later opnieuw.", "SubtitleDownloadFailureForItem": "Downloaden van ondertiteling voor {0} is mislukt", - "SubtitleDownloadFailureFromForItem": "Ondertitels konden niet gedownload worden van {0} voor {1}", + "SubtitleDownloadFailureFromForItem": "Ondertiteling kon niet gedownload worden van {0} voor {1}", "Sync": "Synchronisatie", "System": "Systeem", "TvShows": "TV-series", "User": "Gebruiker", "UserCreatedWithName": "Gebruiker {0} is aangemaakt", "UserDeletedWithName": "Gebruiker {0} is verwijderd", - "UserDownloadingItemWithValues": "{0} download {1}", + "UserDownloadingItemWithValues": "{0} downloadt {1}", "UserLockedOutWithName": "Gebruikersaccount {0} is vergrendeld", "UserOfflineFromDevice": "Verbinding van {0} met {1} is verbroken", "UserOnlineFromDevice": "{0} heeft verbinding met {1}", @@ -90,7 +90,7 @@ "UserStartedPlayingItemWithValues": "{0} speelt {1} af op {2}", "UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt op {2}", "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek", - "ValueSpecialEpisodeName": "Speciaal - {0}", + "ValueSpecialEpisodeName": "Special - {0}", "VersionNumber": "Versie {0}", "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar ontbrekende ondertiteling gebaseerd op metadataconfiguratie.", "TaskDownloadMissingSubtitles": "Ontbrekende ondertiteling downloaden", @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Geluidsnormalisatie", "TaskAudioNormalizationDescription": "Scant bestanden op gegevens voor geluidsnormalisatie.", "TaskDownloadMissingLyrics": "Ontbrekende liedteksten downloaden", - "TaskDownloadMissingLyricsDescription": "Downloadt liedteksten" + "TaskDownloadMissingLyricsDescription": "Downloadt liedteksten", + "TaskExtractMediaSegmentsDescription": "Verkrijgt mediasegmenten vanuit plug-ins met MediaSegment-ondersteuning.", + "TaskMoveTrickplayImages": "Locatie trickplay-afbeeldingen migreren", + "TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.", + "TaskExtractMediaSegments": "Scannen op mediasegmenten" } diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index a24a837ab..33b0bb7e1 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -132,5 +132,9 @@ "TaskAudioNormalization": "Normalizacja dźwięku", "TaskAudioNormalizationDescription": "Skanuje pliki w poszukiwaniu danych normalizacji dźwięku.", "TaskDownloadMissingLyrics": "Pobierz brakujące słowa", - "TaskDownloadMissingLyricsDescription": "Pobierz słowa piosenek" + "TaskDownloadMissingLyricsDescription": "Pobierz słowa piosenek", + "TaskExtractMediaSegments": "Skanowanie segmentów mediów", + "TaskMoveTrickplayImages": "Migruj lokalizację obrazu Trickplay", + "TaskExtractMediaSegmentsDescription": "Wyodrębnia lub pobiera segmenty mediów z wtyczek obsługujących MediaSegment.", + "TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki." } diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index d9867f5e0..9f4f58cb6 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -8,7 +8,7 @@ "CameraImageUploadedFrom": "Uma nova imagem da câmera foi enviada de {0}", "Channels": "Canais", "ChapterNameValue": "Capítulo {0}", - "Collections": "Coletâneas", + "Collections": "Coleções", "DeviceOfflineWithName": "{0} se desconectou", "DeviceOnlineWithName": "{0} se conectou", "FailedLoginAttemptWithUserName": "Falha na tentativa de login de {0}", @@ -130,5 +130,11 @@ "TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists", "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.", "TaskAudioNormalization": "Normalização de áudio", - "TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio." + "TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio.", + "TaskDownloadMissingLyricsDescription": "Baixar letras para músicas", + "TaskDownloadMissingLyrics": "Baixar letra faltante", + "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" } diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index ff9a0d4f4..7e9be76e5 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -129,5 +129,11 @@ "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução", "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.", - "TaskAudioNormalization": "Normalização de áudio" + "TaskAudioNormalization": "Normalização de áudio", + "TaskDownloadMissingLyrics": "Baixar letras faltantes", + "TaskDownloadMissingLyricsDescription": "Baixa letras para músicas", + "TaskMoveTrickplayImagesDescription": "Transfere ficheiros de miniatura de vídeo, conforme as definições da biblioteca.", + "TaskExtractMediaSegments": "Varrimento de segmentos da média", + "TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de média de extensões com suporte a MediaSegment.", + "TaskMoveTrickplayImages": "Migração de miniaturas de vídeo" } diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 2f52aafa3..bf59e1583 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -129,5 +129,11 @@ "TaskAudioNormalizationDescription": "Scanează fișiere pentru date necesare normalizării sunetului.", "TaskAudioNormalization": "Normalizare sunet", "TaskCleanCollectionsAndPlaylists": "Curăță colecțiile și listele de redare", - "TaskCleanCollectionsAndPlaylistsDescription": "Elimină elementele care nu mai există din colecții și liste de redare." + "TaskCleanCollectionsAndPlaylistsDescription": "Elimină elementele care nu mai există din colecții și liste de redare.", + "TaskExtractMediaSegments": "Scanează segmentele media", + "TaskMoveTrickplayImagesDescription": "Mută fișierele trickplay existente conform setărilor librăriei.", + "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" } diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index a9b6fbeef..66d8bf899 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -130,5 +130,11 @@ "TaskCleanCollectionsAndPlaylists": "Vyčistiť kolekcie a playlisty", "TaskCleanCollectionsAndPlaylistsDescription": "Odstráni položky z kolekcií a playlistov, ktoré už neexistujú.", "TaskAudioNormalization": "Normalizácia zvuku", - "TaskAudioNormalizationDescription": "Skenovať súbory za účelom normalizácie zvuku." + "TaskAudioNormalizationDescription": "Skenovať súbory za účelom normalizácie zvuku.", + "TaskExtractMediaSegments": "Skenovanie segmentov médií", + "TaskExtractMediaSegmentsDescription": "Extrahuje alebo získava segmenty médií zo zásuvných modulov s povolenou funkciou MediaSegment.", + "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" } diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index 110af11b7..19be1a23e 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -3,7 +3,7 @@ "AppDeviceValues": "Aplikacija: {0}, Naprava: {1}", "Application": "Aplikacija", "Artists": "Izvajalci", - "AuthenticationSucceededWithUserName": "{0} se je uspešno prijavil", + "AuthenticationSucceededWithUserName": "{0} se je uspešno prijavil/a", "Books": "Knjige", "CameraImageUploadedFrom": "Nova fotografija je bila naložena iz {0}", "Channels": "Kanali", diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index a4e2302d1..5cf54522b 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -9,7 +9,7 @@ "Channels": "Kanaler", "ChapterNameValue": "Kapitel {0}", "Collections": "Samlingar", - "DeviceOfflineWithName": "{0} har avbrutit uppkopplingen", + "DeviceOfflineWithName": "{0} har kopplat ned", "DeviceOnlineWithName": "{0} är ansluten", "FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}", "Favorites": "Favoriter", @@ -121,7 +121,7 @@ "Default": "Standard", "TaskOptimizeDatabase": "Optimera databasen", "TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna aktivitet efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats.", - "TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna rutin kan ta lång tid.", + "TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna körning kan ta lång tid.", "TaskKeyframeExtractor": "Extraktor för nyckelbildrutor", "External": "Extern", "HearingImpaired": "Hörselskadad", @@ -132,5 +132,9 @@ "TaskCleanCollectionsAndPlaylistsDescription": "Tar bort objekt från samlingar och spellistor som inte längre finns.", "TaskAudioNormalizationDescription": "Skannar filer för ljudnormaliseringsdata.", "TaskDownloadMissingLyrics": "Ladda ner saknad låttext", - "TaskDownloadMissingLyricsDescription": "Laddar ner låttexter" + "TaskDownloadMissingLyricsDescription": "Laddar ner låttexter", + "TaskExtractMediaSegments": "Skanning av mediesegment", + "TaskExtractMediaSegmentsDescription": "Extraherar eller hämtar ut mediesegmen från tillägg som stöder MediaSegment.", + "TaskMoveTrickplayImages": "Migrera platsen för Trickplay-bilder", + "TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar." } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 1dceadc61..a3cf78fcb 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -130,5 +130,11 @@ "TaskCleanCollectionsAndPlaylistsDescription": "Artık var olmayan koleksiyon ve çalma listelerindeki ögeleri kaldırır.", "TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin", "TaskAudioNormalizationDescription": "Ses normalleştirme verileri için dosyaları tarar.", - "TaskAudioNormalization": "Ses Normalleştirme" + "TaskAudioNormalization": "Ses Normalleştirme", + "TaskExtractMediaSegments": "Medya Segmenti Tarama", + "TaskMoveTrickplayImages": "Trickplay Görsel Konumunu Taşıma", + "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." } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 97bad4532..3fddc2e78 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -131,5 +131,9 @@ "TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.", "TaskAudioNormalization": "Нормалізація аудіо", "TaskDownloadMissingLyrics": "Завантажити відсутні тексти пісень", - "TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень" + "TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень", + "TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.", + "TaskExtractMediaSegments": "Сканування медіа-сегментів", + "TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень", + "TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment." } diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index 32e2f4bab..f890ea74d 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -131,5 +131,9 @@ "TaskAudioNormalization": "Chuẩn Hóa Âm Thanh", "TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh.", "TaskDownloadMissingLyricsDescription": "Tải xuống lời cho bài hát", - "TaskDownloadMissingLyrics": "Tải xuống lời bị thiếu" + "TaskDownloadMissingLyrics": "Tải xuống lời bị thiếu", + "TaskExtractMediaSegmentsDescription": "Trích xuất hoặc lấy các phân đoạn phương tiện từ các plugin hỗ trợ MediaSegment.", + "TaskMoveTrickplayImages": "Di chuyển vị trí hình ảnh Trickplay", + "TaskMoveTrickplayImagesDescription": "Di chuyển các tập tin trickplay hiện có theo cài đặt thư viện.", + "TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 4bec590fb..9a0e2115e 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -93,7 +93,7 @@ "ValueSpecialEpisodeName": "特典 - {0}", "VersionNumber": "版本 {0}", "TaskUpdatePluginsDescription": "为已设置为自动更新的插件下载和安装更新。", - "TaskRefreshPeople": "刷新人员", + "TaskRefreshPeople": "刷新演职人员", "TasksChannelsCategory": "互联网频道", "TasksLibraryCategory": "媒体库", "TaskDownloadMissingSubtitlesDescription": "根据元数据设置在互联网上搜索缺少的字幕。", @@ -122,15 +122,19 @@ "TaskOptimizeDatabaseDescription": "压缩数据库并优化可用空间,在扫描库或执行其他数据库修改后运行此任务可能会提高性能。", "TaskOptimizeDatabase": "优化数据库", "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的 HLS 播放列表。这项任务可能需要很长时间。", - "TaskKeyframeExtractor": "关键帧提取器", + "TaskKeyframeExtractor": "关键帧提取", "External": "外部", "HearingImpaired": "听力障碍", - "TaskRefreshTrickplayImages": "生成时间轴缩略图", - "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。", + "TaskRefreshTrickplayImages": "生成进度条预览图", + "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成进度条预览图。", "TaskCleanCollectionsAndPlaylists": "清理合集和播放列表", "TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。", "TaskAudioNormalization": "音频标准化", "TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。", "TaskDownloadMissingLyrics": "下载缺失的歌词", - "TaskDownloadMissingLyricsDescription": "下载歌曲歌词" + "TaskDownloadMissingLyricsDescription": "下载歌曲歌词", + "TaskMoveTrickplayImages": "迁移进度条预览图的存储位置", + "TaskExtractMediaSegments": "媒体片段扫描", + "TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体片段。", + "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index f06bbc591..81d5b83d6 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -129,5 +129,11 @@ "TaskCleanCollectionsAndPlaylists": "清理系列和播放清單", "TaskCleanCollectionsAndPlaylistsDescription": "清理系列和播放清單中已不存在的項目。", "TaskAudioNormalization": "音量標準化", - "TaskAudioNormalizationDescription": "掃描文件以找出音量標準化資料。" + "TaskAudioNormalizationDescription": "掃描文件以找出音量標準化資料。", + "TaskDownloadMissingLyrics": "下載缺少的歌詞", + "TaskDownloadMissingLyricsDescription": "卡在歌曲歌詞", + "TaskExtractMediaSegments": "掃描媒體片段", + "TaskExtractMediaSegmentsDescription": "從使用媒體片段的擴充功能取得媒體片段。", + "TaskMoveTrickplayImages": "遷移快轉縮圖位置", + "TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。" } diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 896f47923..eb55e32c5 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -91,8 +91,29 @@ namespace Emby.Server.Implementations.MediaEncoder return video.DefaultVideoStreamIndex.HasValue; } + private long GetAverageDurationBetweenChapters(IReadOnlyList<ChapterInfo> chapters) + { + if (chapters.Count < 2) + { + return 0; + } + + long sum = 0; + for (int i = 1; i < chapters.Count; i++) + { + sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks; + } + + return sum / chapters.Count; + } + public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) { + if (chapters.Count == 0) + { + return true; + } + var libraryOptions = _libraryManager.GetLibraryOptions(video); if (!IsEligibleForChapterImageExtraction(video, libraryOptions)) @@ -100,6 +121,14 @@ namespace Emby.Server.Implementations.MediaEncoder extractImages = false; } + var averageChapterDuration = GetAverageDurationBetweenChapters(chapters); + var threshold = TimeSpan.FromSeconds(1).Ticks; + if (averageChapterDuration < threshold) + { + _logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold); + extractImages = false; + } + var success = true; var changesMade = false; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs new file mode 100644 index 000000000..d6fad7526 --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Task to obtain media segments. +/// </summary> +public class MediaSegmentExtractionTask : IScheduledTask +{ + /// <summary> + /// The library manager. + /// </summary> + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + private readonly IMediaSegmentManager _mediaSegmentManager; + private static readonly BaseItemKind[] _itemTypes = [BaseItemKind.Episode, BaseItemKind.Movie, BaseItemKind.Audio, BaseItemKind.AudioBook]; + + /// <summary> + /// Initializes a new instance of the <see cref="MediaSegmentExtractionTask" /> class. + /// </summary> + /// <param name="libraryManager">The library manager.</param> + /// <param name="localization">The localization manager.</param> + /// <param name="mediaSegmentManager">The segment manager.</param> + public MediaSegmentExtractionTask(ILibraryManager libraryManager, ILocalizationManager localization, IMediaSegmentManager mediaSegmentManager) + { + _libraryManager = libraryManager; + _localization = localization; + _mediaSegmentManager = mediaSegmentManager; + } + + /// <inheritdoc/> + public string Name => _localization.GetLocalizedString("TaskExtractMediaSegments"); + + /// <inheritdoc/> + public string Description => _localization.GetLocalizedString("TaskExtractMediaSegmentsDescription"); + + /// <inheritdoc/> + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + + /// <inheritdoc/> + public string Key => "TaskExtractMediaSegments"; + + /// <inheritdoc/> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + progress.Report(0); + + var pagesize = 100; + + var query = new InternalItemsQuery + { + MediaTypes = new[] { MediaType.Video, MediaType.Audio }, + IsVirtualItem = false, + IncludeItemTypes = _itemTypes, + DtoOptions = new DtoOptions(true), + SourceTypes = new[] { SourceType.Library }, + Recursive = true, + Limit = pagesize + }; + + var numberOfVideos = _libraryManager.GetCount(query); + + var startIndex = 0; + var numComplete = 0; + + while (startIndex < numberOfVideos) + { + query.StartIndex = startIndex; + + var baseItems = _libraryManager.GetItemList(query); + var currentPageCount = baseItems.Count; + // TODO parallelize with Parallel.ForEach? + for (var i = 0; i < currentPageCount; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var item = baseItems[i]; + // Only local files supported + if (item.IsFileProtocol && File.Exists(item.Path)) + { + await _mediaSegmentManager.RunSegmentPluginProviders(item, false, cancellationToken).ConfigureAwait(false); + } + + // Update progress + numComplete++; + double percent = (double)numComplete / numberOfVideos; + progress.Report(100 * percent); + } + + startIndex += pagesize; + } + + progress.Report(100); + } + + /// <inheritdoc/> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + yield return new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, + IntervalTicks = TimeSpan.FromHours(12).Ticks + }; + } +} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 72e164b52..6a8ad2bdc 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -68,13 +66,29 @@ namespace Emby.Server.Implementations.Session private Timer _inactiveTimer; private DtoOptions _itemInfoDtoOptions; - private bool _disposed = false; + private bool _disposed; + /// <summary> + /// Initializes a new instance of the <see cref="SessionManager"/> class. + /// </summary> + /// <param name="logger">Instance of <see cref="ILogger{SessionManager}"/> interface.</param> + /// <param name="eventManager">Instance of <see cref="IEventManager"/> interface.</param> + /// <param name="userDataManager">Instance of <see cref="IUserDataManager"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> + /// <param name="userManager">Instance of <see cref="IUserManager"/> interface.</param> + /// <param name="musicManager">Instance of <see cref="IMusicManager"/> interface.</param> + /// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param> + /// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param> + /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> + /// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param> + /// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param> + /// <param name="hostApplicationLifetime">Instance of <see cref="IHostApplicationLifetime"/> interface.</param> public SessionManager( ILogger<SessionManager> logger, IEventManager eventManager, IUserDataManager userDataManager, - IServerConfigurationManager config, + IServerConfigurationManager serverConfigurationManager, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, @@ -88,7 +102,7 @@ namespace Emby.Server.Implementations.Session _logger = logger; _eventManager = eventManager; _userDataManager = userDataManager; - _config = config; + _config = serverConfigurationManager; _libraryManager = libraryManager; _userManager = userManager; _musicManager = musicManager; @@ -508,7 +522,10 @@ namespace Emby.Server.Implementations.Session deviceName = "Network Device"; } - var deviceOptions = _deviceManager.GetDeviceOptions(deviceId); + var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new() + { + DeviceId = deviceId + }; if (string.IsNullOrEmpty(deviceOptions.CustomName)) { sessionInfo.DeviceName = deviceName; @@ -1076,6 +1093,42 @@ namespace Emby.Server.Implementations.Session return session; } + private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo) + { + return new SessionInfoDto + { + PlayState = sessionInfo.PlayState, + AdditionalUsers = sessionInfo.AdditionalUsers, + Capabilities = _deviceManager.ToClientCapabilitiesDto(sessionInfo.Capabilities), + RemoteEndPoint = sessionInfo.RemoteEndPoint, + PlayableMediaTypes = sessionInfo.PlayableMediaTypes, + Id = sessionInfo.Id, + UserId = sessionInfo.UserId, + UserName = sessionInfo.UserName, + Client = sessionInfo.Client, + LastActivityDate = sessionInfo.LastActivityDate, + LastPlaybackCheckIn = sessionInfo.LastPlaybackCheckIn, + LastPausedDate = sessionInfo.LastPausedDate, + DeviceName = sessionInfo.DeviceName, + DeviceType = sessionInfo.DeviceType, + NowPlayingItem = sessionInfo.NowPlayingItem, + NowViewingItem = sessionInfo.NowViewingItem, + DeviceId = sessionInfo.DeviceId, + ApplicationVersion = sessionInfo.ApplicationVersion, + TranscodingInfo = sessionInfo.TranscodingInfo, + IsActive = sessionInfo.IsActive, + SupportsMediaControl = sessionInfo.SupportsMediaControl, + SupportsRemoteControl = sessionInfo.SupportsRemoteControl, + NowPlayingQueue = sessionInfo.NowPlayingQueue, + NowPlayingQueueFullItems = sessionInfo.NowPlayingQueueFullItems, + HasCustomDeviceName = sessionInfo.HasCustomDeviceName, + PlaylistItemId = sessionInfo.PlaylistItemId, + ServerId = sessionInfo.ServerId, + UserPrimaryImageTag = sessionInfo.UserPrimaryImageTag, + SupportedCommands = sessionInfo.SupportedCommands + }; + } + /// <inheritdoc /> public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken) { @@ -1393,7 +1446,7 @@ namespace Emby.Server.Implementations.Session UserName = user.Username }; - session.AdditionalUsers = [..session.AdditionalUsers, newUser]; + session.AdditionalUsers = [.. session.AdditionalUsers, newUser]; } } @@ -1505,7 +1558,7 @@ namespace Emby.Server.Implementations.Session var returnResult = new AuthenticationResult { User = _userManager.GetUserDto(user, request.RemoteEndPoint), - SessionInfo = session, + SessionInfo = ToSessionInfoDto(session), AccessToken = token, ServerId = _appHost.SystemId }; @@ -1800,6 +1853,105 @@ namespace Emby.Server.Implementations.Session return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false); } + /// <inheritdoc/> + public IReadOnlyList<SessionInfoDto> GetSessions( + Guid userId, + string deviceId, + int? activeWithinSeconds, + Guid? controllableUserToCheck, + bool isApiKey) + { + var result = Sessions; + if (!string.IsNullOrEmpty(deviceId)) + { + result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); + } + + var userCanControlOthers = false; + var userIsAdmin = false; + User user = null; + + if (isApiKey) + { + userCanControlOthers = true; + userIsAdmin = true; + } + else if (!userId.IsEmpty()) + { + user = _userManager.GetUserById(userId); + if (user is not null) + { + userCanControlOthers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers); + userIsAdmin = user.HasPermission(PermissionKind.IsAdministrator); + } + else + { + return []; + } + } + + if (!controllableUserToCheck.IsNullOrEmpty()) + { + result = result.Where(i => i.SupportsRemoteControl); + + var controlledUser = _userManager.GetUserById(controllableUserToCheck.Value); + if (controlledUser is null) + { + return []; + } + + if (!controlledUser.HasPermission(PermissionKind.EnableSharedDeviceControl)) + { + // Controlled user has device sharing disabled + result = result.Where(i => !i.UserId.IsEmpty()); + } + + if (!userCanControlOthers) + { + // User cannot control other user's sessions, validate user id. + result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId)); + } + + result = result.Where(i => + { + if (isApiKey) + { + return true; + } + + if (user is null) + { + return false; + } + + return string.IsNullOrWhiteSpace(i.DeviceId) || _deviceManager.CanAccessDevice(user, i.DeviceId); + }); + } + else if (!userIsAdmin) + { + // Request isn't from administrator, limit to "own" sessions. + result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(userId)); + } + + if (!userIsAdmin) + { + // Don't report acceleration type for non-admin users. + result = result.Select(r => + { + r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none; + return r; + }); + } + + if (activeWithinSeconds.HasValue && activeWithinSeconds.Value > 0) + { + var minActiveDate = DateTime.UtcNow.AddSeconds(0 - activeWithinSeconds.Value); + result = result.Where(i => i.LastActivityDate >= minActiveDate); + } + + return result.Select(ToSessionInfoDto).ToList(); + } + /// <inheritdoc /> public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken) { |
