aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server.Implementations')
-rw-r--r--Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs21
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs27
-rw-r--r--Jellyfin.Server.Implementations/Item/PeopleRepository.cs11
-rw-r--r--Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs12
-rw-r--r--Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs57
-rw-r--r--Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs102
6 files changed, 160 insertions, 70 deletions
diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
index 30094a88c0..a6dc5458ee 100644
--- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
+++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
@@ -118,15 +118,21 @@ public class BackupService : IBackupService
throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version.");
}
- void CopyDirectory(string source, string target)
+ void CopyDirectory(string source, string target, string[]? exclude = null)
{
var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar);
var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar;
+ var excludePaths = exclude?.Select(e => $"{source}/{e}/").ToArray();
foreach (var item in zipArchive.Entries)
{
var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName));
var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName)));
+ if (excludePaths is not null && excludePaths.Any(e => item.FullName.StartsWith(e, StringComparison.Ordinal)))
+ {
+ continue;
+ }
+
if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal)
|| !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)
|| Path.EndsInDirectorySeparator(item.FullName))
@@ -142,8 +148,10 @@ public class BackupService : IBackupService
}
CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath);
- CopyDirectory("Data", _applicationPaths.DataPath);
+ CopyDirectory("Data", _applicationPaths.DataPath, exclude: ["metadata", "metadata-default"]);
CopyDirectory("Root", _applicationPaths.RootFolderPath);
+ CopyDirectory("Data/metadata", _applicationPaths.InternalMetadataPath);
+ CopyDirectory("Data/metadata-default", _applicationPaths.DefaultInternalMetadataPath);
if (manifest.Options.Database)
{
@@ -404,6 +412,15 @@ public class BackupService : IBackupService
if (backupOptions.Metadata)
{
CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata"));
+
+ // If a custom metadata path is configured, the default location may still contain data.
+ if (!string.Equals(
+ Path.GetFullPath(_applicationPaths.DefaultInternalMetadataPath),
+ Path.GetFullPath(_applicationPaths.InternalMetadataPath),
+ StringComparison.OrdinalIgnoreCase))
+ {
+ CopyDirectory(Path.Combine(_applicationPaths.DefaultInternalMetadataPath), Path.Combine("Data", "metadata-default"));
+ }
}
var manifestStream = await zipArchive.CreateEntry(ManifestEntryName).OpenAsync().ConfigureAwait(false);
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 5bb4494dd2..cd28c6e43e 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -683,14 +683,15 @@ public sealed class BaseItemRepository
.SelectMany(f => f.Values)
.Distinct()
.ToArray();
+
+ var types = allListedItemValues.Select(e => e.MagicNumber).Distinct().ToArray();
+ var values = allListedItemValues.Select(e => e.Value).Distinct().ToArray();
+ var allListedItemValuesSet = allListedItemValues.ToHashSet();
+
var existingValues = context.ItemValues
- .Select(e => new
- {
- item = e,
- Key = e.Type + "+" + e.Value
- })
- .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
- .Select(e => e.item)
+ .Where(e => types.Contains(e.Type) && values.Contains(e.Value))
+ .AsEnumerable()
+ .Where(e => allListedItemValuesSet.Contains((e.Type, e.Value)))
.ToArray();
var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).Select(f => new ItemValue()
{
@@ -1050,7 +1051,7 @@ public sealed class BaseItemRepository
entity.TotalBitrate = dto.TotalBitrate;
entity.ExternalId = dto.ExternalId;
entity.Size = dto.Size;
- entity.Genres = string.Join('|', dto.Genres);
+ entity.Genres = string.Join('|', dto.Genres.Distinct(StringComparer.OrdinalIgnoreCase));
entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
entity.ChannelId = dto.ChannelId;
@@ -1077,9 +1078,9 @@ public sealed class BaseItemRepository
}
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
- entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null;
- entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
- entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
+ entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p)).Distinct(StringComparer.OrdinalIgnoreCase)) : null;
+ entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
+ entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
.Select(e => new BaseItemMetadataField()
{
@@ -1122,12 +1123,12 @@ public sealed class BaseItemRepository
if (dto is IHasArtist hasArtists)
{
- entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
+ entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
}
if (dto is IHasAlbumArtist hasAlbumArtists)
{
- entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null;
+ entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
}
if (dto is LiveTvProgram program)
diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
index e2569241d2..ad9953d1b6 100644
--- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
@@ -62,7 +62,11 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
using var context = _dbProvider.CreateDbContext();
var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct();
- // dbQuery = dbQuery.OrderBy(e => e.ListOrder);
+ if (filter.StartIndex.HasValue && filter.StartIndex > 0)
+ {
+ dbQuery = dbQuery.Skip(filter.StartIndex.Value);
+ }
+
if (filter.Limit > 0)
{
dbQuery = dbQuery.Take(filter.Limit);
@@ -197,6 +201,11 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId)));
}
+ if (filter.ParentId != null)
+ {
+ query = query.Where(e => e.BaseItems!.Any(w => context.AncestorIds.Any(i => i.ParentItemId == filter.ParentId && i.ItemId == w.ItemId)));
+ }
+
if (!filter.AppearsInItemId.IsEmpty())
{
query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId)));
diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
index d00c87463c..c514735688 100644
--- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
+++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs
@@ -182,6 +182,18 @@ public class MediaSegmentManager : IMediaSegmentManager
/// <inheritdoc />
public async Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken)
{
+ foreach (var provider in _segmentProviders)
+ {
+ try
+ {
+ await provider.CleanupExtractedData(itemId, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Provider {ProviderName} failed to clean up extracted data for item {ItemId}", provider.Name, itemId);
+ }
+ }
+
var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (db.ConfigureAwait(false))
{
diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
index ce628a04d0..13c7895f83 100644
--- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
+++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
@@ -28,22 +28,44 @@ public static class StorageHelper
}
/// <summary>
- /// Gets the free space of a specific directory.
+ /// Gets the free space of the parent filesystem of a specific directory.
/// </summary>
/// <param name="path">Path to a folder.</param>
- /// <returns>The number of bytes available space.</returns>
+ /// <returns>Various details about the parent filesystem containing the directory.</returns>
public static FolderStorageInfo GetFreeSpaceOf(string path)
{
try
{
- var driveInfo = new DriveInfo(path);
+ // Fully resolve the given path to an actual filesystem target, in case it's a symlink or similar.
+ var resolvedPath = ResolvePath(path);
+ // We iterate all filesystems reported by GetDrives() here, and attempt to find the best
+ // match that contains, as deep as possible, the given path.
+ // This is required because simply calling `DriveInfo` on a path returns that path as
+ // the Name and RootDevice, which is not at all how this should work.
+ var allDrives = DriveInfo.GetDrives();
+ DriveInfo? bestMatch = null;
+ foreach (DriveInfo d in allDrives)
+ {
+ if (resolvedPath.StartsWith(d.RootDirectory.FullName, StringComparison.InvariantCultureIgnoreCase) &&
+ (bestMatch is null || d.RootDirectory.FullName.Length > bestMatch.RootDirectory.FullName.Length))
+ {
+ bestMatch = d;
+ }
+ }
+
+ if (bestMatch is null)
+ {
+ throw new InvalidOperationException($"The path `{path}` has no matching parent device. Space check invalid.");
+ }
+
return new FolderStorageInfo()
{
Path = path,
- FreeSpace = driveInfo.AvailableFreeSpace,
- UsedSpace = driveInfo.TotalSize - driveInfo.AvailableFreeSpace,
- StorageType = driveInfo.DriveType.ToString(),
- DeviceId = driveInfo.Name,
+ ResolvedPath = resolvedPath,
+ FreeSpace = bestMatch.AvailableFreeSpace,
+ UsedSpace = bestMatch.TotalSize - bestMatch.AvailableFreeSpace,
+ StorageType = bestMatch.DriveType.ToString(),
+ DeviceId = bestMatch.Name,
};
}
catch
@@ -51,6 +73,7 @@ public static class StorageHelper
return new FolderStorageInfo()
{
Path = path,
+ ResolvedPath = path,
FreeSpace = -1,
UsedSpace = -1,
StorageType = null,
@@ -60,6 +83,26 @@ public static class StorageHelper
}
/// <summary>
+ /// Walk a path and fully resolve any symlinks within it.
+ /// </summary>
+ private static string ResolvePath(string path)
+ {
+ var parts = path.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
+ var current = Path.DirectorySeparatorChar.ToString();
+ foreach (var part in parts)
+ {
+ current = Path.Combine(current, part);
+ var resolved = new DirectoryInfo(current).ResolveLinkTarget(returnFinalTarget: true);
+ if (resolved is not null)
+ {
+ current = resolved.FullName;
+ }
+ }
+
+ return current;
+ }
+
+ /// <summary>
/// Gets the underlying drive data from a given path and checks if the available storage capacity matches the threshold.
/// </summary>
/// <param name="path">The path to a folder to evaluate.</param>
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index 4505a377ce..63319831e1 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -399,64 +399,72 @@ public class TrickplayManager : ITrickplayManager
var workDir = Path.Combine(_appPaths.TempDirectory, "trickplay_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
- var trickplayInfo = new TrickplayInfo
+ try
{
- Width = width,
- Interval = options.Interval,
- TileWidth = options.TileWidth,
- TileHeight = options.TileHeight,
- ThumbnailCount = images.Count,
- // Set during image generation
- Height = 0,
- Bandwidth = 0
- };
-
- /*
- * Generate trickplay tiles from sets of thumbnails
- */
- var imageOptions = new ImageCollageOptions
- {
- Width = trickplayInfo.TileWidth,
- Height = trickplayInfo.TileHeight
- };
+ var trickplayInfo = new TrickplayInfo
+ {
+ Width = width,
+ Interval = options.Interval,
+ TileWidth = options.TileWidth,
+ TileHeight = options.TileHeight,
+ ThumbnailCount = images.Count,
+ // Set during image generation
+ Height = 0,
+ Bandwidth = 0
+ };
+
+ /*
+ * Generate trickplay tiles from sets of thumbnails
+ */
+ var imageOptions = new ImageCollageOptions
+ {
+ Width = trickplayInfo.TileWidth,
+ Height = trickplayInfo.TileHeight
+ };
- var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
- var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
+ var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
+ var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
- for (int i = 0; i < requiredTiles; i++)
- {
- // Set output/input paths
- var tilePath = Path.Combine(workDir, $"{i}.jpg");
+ for (int i = 0; i < requiredTiles; i++)
+ {
+ // Set output/input paths
+ var tilePath = Path.Combine(workDir, $"{i}.jpg");
- imageOptions.OutputPath = tilePath;
- imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList();
+ imageOptions.OutputPath = tilePath;
+ imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList();
- // Generate image and use returned height for tiles info
- var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
- if (trickplayInfo.Height == 0)
- {
- trickplayInfo.Height = height;
+ // Generate image and use returned height for tiles info
+ var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
+ if (trickplayInfo.Height == 0)
+ {
+ trickplayInfo.Height = height;
+ }
+
+ // Update bitrate
+ var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
+ trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
}
- // Update bitrate
- var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
- trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
- }
+ /*
+ * Move trickplay tiles to output directory
+ */
+ Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
+
+ // Replace existing tiles if they already exist
+ if (Directory.Exists(outputDir))
+ {
+ Directory.Delete(outputDir, true);
+ }
- /*
- * Move trickplay tiles to output directory
- */
- Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
+ _fileSystem.MoveDirectory(workDir, outputDir);
- // Replace existing tiles if they already exist
- if (Directory.Exists(outputDir))
+ return trickplayInfo;
+ }
+ catch
{
- Directory.Delete(outputDir, true);
+ Directory.Delete(workDir, true);
+ throw;
}
-
- _fileSystem.MoveDirectory(workDir, outputDir);
-
- return trickplayInfo;
}
private bool CanGenerateTrickplay(Video video, int interval)