Recognize file changes and remove data on change (#13839)
Some checks are pending
CodeQL / Analyze (csharp) (push) Waiting to run
OpenAPI / OpenAPI - HEAD (push) Waiting to run
OpenAPI / OpenAPI - BASE (push) Waiting to run
OpenAPI / OpenAPI - Difference (push) Blocked by required conditions
OpenAPI / OpenAPI - Publish Unstable Spec (push) Blocked by required conditions
OpenAPI / OpenAPI - Publish Stable Spec (push) Blocked by required conditions
Tests / run-tests (macos-latest) (push) Waiting to run
Tests / run-tests (ubuntu-latest) (push) Waiting to run
Tests / run-tests (windows-latest) (push) Waiting to run
Project Automation / Project board (push) Waiting to run
Merge Conflict Labeler / Labeling (push) Waiting to run

This commit is contained in:
Tim Eisele
2025-05-05 05:21:44 +02:00
committed by GitHub
parent 0c3ba30de2
commit d976f13970
57 changed files with 2008 additions and 1218 deletions

View File

@@ -36,7 +36,6 @@ using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.Database.Implementations;
using Jellyfin.Drawing;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager;
@@ -63,6 +62,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
@@ -93,7 +93,6 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -560,6 +559,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
serviceCollection.AddSingleton<IKeyframeManager, KeyframeManager>();
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();

View File

@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
var libraryOptions = new LibraryOptions
{
PathInfos = new[] { new MediaPathInfo(path) },
PathInfos = [new MediaPathInfo(path)],
EnableRealtimeMonitor = false,
SaveLocalMetadata = true
};
@@ -150,15 +150,15 @@ namespace Emby.Server.Implementations.Collections
try
{
Directory.CreateDirectory(path);
var info = Directory.CreateDirectory(path);
var collection = new BoxSet
{
Name = name,
Path = path,
IsLocked = options.IsLocked,
ProviderIds = options.ProviderIds,
DateCreated = DateTime.UtcNow
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc
};
parentFolder.AddChild(collection);

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.MediaEncoding.Keyframes;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Persistence;
namespace Emby.Server.Implementations.Library;
/// <summary>
/// Manager for Keyframe data.
/// </summary>
public class KeyframeManager : IKeyframeManager
{
private readonly IKeyframeRepository _repository;
/// <summary>
/// Initializes a new instance of the <see cref="KeyframeManager"/> class.
/// </summary>
/// <param name="repository">The keyframe repository.</param>
public KeyframeManager(IKeyframeRepository repository)
{
_repository = repository;
}
/// <inheritdoc />
public IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId)
{
return _repository.GetKeyframeData(itemId);
}
/// <inheritdoc />
public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken)
{
await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
{
await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -208,7 +208,7 @@ namespace Emby.Server.Implementations.Library
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
private ILibraryPostScanTask[] PostscanTasks { get; set; } = [];
private ILibraryPostScanTask[] PostScanTasks { get; set; } = [];
/// <summary>
/// Gets or sets the intro providers.
@@ -245,20 +245,20 @@ namespace Emby.Server.Implementations.Library
/// <param name="resolvers">The resolvers.</param>
/// <param name="introProviders">The intro providers.</param>
/// <param name="itemComparers">The item comparers.</param>
/// <param name="postscanTasks">The post scan tasks.</param>
/// <param name="postScanTasks">The post scan tasks.</param>
public void AddParts(
IEnumerable<IResolverIgnoreRule> rules,
IEnumerable<IItemResolver> resolvers,
IEnumerable<IIntroProvider> introProviders,
IEnumerable<IBaseItemComparer> itemComparers,
IEnumerable<ILibraryPostScanTask> postscanTasks)
IEnumerable<ILibraryPostScanTask> postScanTasks)
{
EntityResolutionIgnoreRules = rules.ToArray();
EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
IntroProviders = introProviders.ToArray();
Comparers = itemComparers.ToArray();
PostscanTasks = postscanTasks.ToArray();
PostScanTasks = postScanTasks.ToArray();
}
/// <summary>
@@ -393,7 +393,7 @@ namespace Emby.Server.Implementations.Library
}
}
if (options.DeleteFileLocation && item.IsFileProtocol)
if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item))
{
// Assume only the first is required
// Add this flag to GetDeletePaths if required in the future
@@ -472,6 +472,36 @@ namespace Emby.Server.Implementations.Library
ReportItemRemoved(item, parent);
}
private bool IsInternalItem(BaseItem item)
{
if (!item.IsFileProtocol)
{
return false;
}
var pathToCheck = item switch
{
Genre => _configurationManager.ApplicationPaths.GenrePath,
MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
MusicGenre => _configurationManager.ApplicationPaths.GenrePath,
Person => _configurationManager.ApplicationPaths.PeoplePath,
Studio => _configurationManager.ApplicationPaths.StudioPath,
Year => _configurationManager.ApplicationPaths.YearPath,
_ => null
};
var itemPath = item.Path;
if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
{
var cleanPath = _fileSystem.GetValidFilename(itemPath);
var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
}
return false;
}
private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
{
var list = GetInternalMetadataPaths(item);
@@ -639,7 +669,7 @@ namespace Emby.Server.Implementations.Library
}
}
// Need to remove subpaths that may have been resolved from shortcuts
// Need to remove sub-paths that may have been resolved from shortcuts
// Example: if \\server\movies exists, then strip out \\server\movies\action
if (isPhysicalRoot)
{
@@ -772,11 +802,12 @@ namespace Emby.Server.Implementations.Library
// Add in the plug-in folders
var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
Directory.CreateDirectory(path);
var info = Directory.CreateDirectory(path);
Folder folder = new PlaylistsFolder
{
Path = path
Path = path,
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc,
};
if (folder.Id.IsEmpty())
@@ -862,7 +893,7 @@ namespace Emby.Server.Implementations.Library
{
Path = path,
IsFolder = isFolder,
OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) },
OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
Limit = 1,
DtoOptions = new DtoOptions(true)
};
@@ -968,7 +999,7 @@ namespace Emby.Server.Implementations.Library
{
var existing = GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
IncludeItemTypes = [BaseItemKind.MusicArtist],
Name = name,
DtoOptions = options
}).Cast<MusicArtist>()
@@ -987,12 +1018,13 @@ namespace Emby.Server.Implementations.Library
var item = GetItemById(id) as T;
if (item is null)
{
var info = Directory.CreateDirectory(path);
item = new T
{
Name = name,
Id = id,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc,
Path = path
};
@@ -1118,7 +1150,7 @@ namespace Emby.Server.Implementations.Library
/// <returns>Task.</returns>
private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
{
var tasks = PostscanTasks.ToList();
var tasks = PostScanTasks.ToList();
var numComplete = 0;
var numTasks = tasks.Count;
@@ -1241,7 +1273,7 @@ namespace Emby.Server.Implementations.Library
private CollectionTypeOptions? GetCollectionType(string path)
{
var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
foreach (ReadOnlySpan<char> file in files)
{
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
@@ -1312,7 +1344,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
SetTopParentIdsOrAncestors(query, new[] { parent });
SetTopParentIdsOrAncestors(query, [parent]);
}
}
@@ -1343,7 +1375,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
SetTopParentIdsOrAncestors(query, new[] { parent });
SetTopParentIdsOrAncestors(query, [parent]);
}
}
@@ -1531,7 +1563,7 @@ namespace Emby.Server.Implementations.Library
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
SetTopParentIdsOrAncestors(query, new[] { parent });
SetTopParentIdsOrAncestors(query, [parent]);
}
}
@@ -1561,7 +1593,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
query.TopParentIds = new[] { Guid.NewGuid() };
query.TopParentIds = [Guid.NewGuid()];
}
}
else
@@ -1572,7 +1604,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.AncestorIds.Length == 0)
{
query.AncestorIds = new[] { Guid.NewGuid() };
query.AncestorIds = [Guid.NewGuid()];
}
}
@@ -1601,7 +1633,7 @@ namespace Emby.Server.Implementations.Library
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
query.TopParentIds = new[] { Guid.NewGuid() };
query.TopParentIds = [Guid.NewGuid()];
}
}
}
@@ -1612,7 +1644,7 @@ namespace Emby.Server.Implementations.Library
{
if (view.ViewType == CollectionType.livetv)
{
return new[] { view.Id };
return [view.Id];
}
// Translate view into folders
@@ -1661,7 +1693,7 @@ namespace Emby.Server.Implementations.Library
var topParent = item.GetTopParent();
if (topParent is not null)
{
return new[] { topParent.Id };
return [topParent.Id];
}
return [];
@@ -1868,7 +1900,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public void CreateItem(BaseItem item, BaseItem? parent)
{
CreateItems(new[] { item }, parent, CancellationToken.None);
CreateItems([item], parent, CancellationToken.None);
}
/// <inheritdoc />
@@ -2054,7 +2086,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
{
@@ -2283,13 +2315,13 @@ namespace Emby.Server.Implementations.Library
if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
{
Directory.CreateDirectory(path);
var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
DateCreated = DateTime.UtcNow,
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
ForcedSortName = sortName
@@ -2331,13 +2363,13 @@ namespace Emby.Server.Implementations.Library
if (item is null)
{
Directory.CreateDirectory(path);
var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
DateCreated = DateTime.UtcNow,
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
ForcedSortName = sortName,
@@ -2395,20 +2427,19 @@ namespace Emby.Server.Implementations.Library
if (item is null)
{
Directory.CreateDirectory(path);
var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
DateCreated = DateTime.UtcNow,
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
ForcedSortName = sortName
ForcedSortName = sortName,
DisplayParentId = parentId
};
item.DisplayParentId = parentId;
CreateItem(item, null);
isNew = true;
@@ -2465,20 +2496,19 @@ namespace Emby.Server.Implementations.Library
if (item is null)
{
Directory.CreateDirectory(path);
var info = Directory.CreateDirectory(path);
item = new UserView
{
Path = path,
Id = id,
DateCreated = DateTime.UtcNow,
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc,
Name = name,
ViewType = viewType,
ForcedSortName = sortName
ForcedSortName = sortName,
DisplayParentId = parentId
};
item.DisplayParentId = parentId;
CreateItem(item, null);
isNew = true;
@@ -2989,12 +3019,14 @@ namespace Emby.Server.Implementations.Library
if (personEntity is null)
{
var path = Person.GetPath(person.Name);
var info = Directory.CreateDirectory(path);
var lastWriteTime = info.LastWriteTimeUtc;
personEntity = new Person()
{
Name = person.Name,
Id = GetItemByNameId<Person>(path),
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
DateCreated = info.CreationTimeUtc,
DateModified = lastWriteTime,
Path = path
};

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using MediaBrowser.Common.Configuration;
@@ -84,4 +85,17 @@ public class PathManager : IPathManager
return Path.Combine(GetChapterImageFolderPath(item), filename);
}
/// <inheritdoc/>
public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
{
var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
return [
GetAttachmentFolderPath(mediaSourceId),
GetSubtitleFolderPath(mediaSourceId),
GetTrickplayDirectory(item, false),
GetTrickplayDirectory(item, true),
GetChapterImageFolderPath(item)
];
}
}

View File

@@ -136,23 +136,33 @@ namespace Emby.Server.Implementations.Library
if (config.UseFileCreationTimeForDateAdded)
{
// directoryService.getFile may return null
if (info is not null)
var fileCreationDate = info?.CreationTimeUtc;
if (fileCreationDate is not null)
{
var dateCreated = info.CreationTimeUtc;
var dateCreated = fileCreationDate;
if (dateCreated.Equals(DateTime.MinValue))
{
dateCreated = DateTime.UtcNow;
}
item.DateCreated = dateCreated;
item.DateCreated = dateCreated.Value;
}
}
else
{
item.DateCreated = DateTime.UtcNow;
}
if (info is not null && !info.IsDirectory)
{
item.Size = info.Length;
}
var fileModificationDate = info?.LastWriteTimeUtc;
if (fileModificationDate.HasValue)
{
item.DateModified = fileModificationDate.Value;
}
}
}
}

View File

@@ -134,14 +134,16 @@ namespace Emby.Server.Implementations.Playlists
try
{
Directory.CreateDirectory(path);
var info = Directory.CreateDirectory(path);
var playlist = new Playlist
{
Name = name,
Path = path,
OwnerUserId = request.UserId,
Shares = request.Users ?? [],
OpenAccess = request.Public ?? false
OpenAccess = request.Public ?? false,
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc
};
playlist.SetMediaType(request.MediaType);

View File

@@ -4,10 +4,10 @@ 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.Controller.MediaSegments;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;