Merge pull request 'pgsql_conversion' (#5) from pgsql_conversion into main

Reviewed-on: #5
This commit is contained in:
2026-02-22 12:10:27 -05:00
105 changed files with 1366 additions and 822 deletions
+31 -7
View File
@@ -1,13 +1,38 @@
# EditorConfig for MediaBrowser.Model# https://EditorConfig.orgroot = true[*]charset = utf-8insert_final_newline = truetrim_trailing_whitespace = true[*.cs]indent_size = 4indent_style = spacedotnet_sort_system_directives_first = truedotnet_separate_import_directive_groups = false# StyleCop Analyzer Rules - Disabled for project consistencydotnet_diagnostic.SA1101.severity = nonedotnet_diagnostic.SA1309.severity = nonedotnet_diagnostic.SA1204.severity = nonedotnet_diagnostic.SA1202.severity = nonedotnet_diagnostic.SA1135.severity = nonedotnet_diagnostic.SA1600.severity = suggestion# StyleCop Analyzer Rules - Keep enableddotnet_diagnostic.SA1413.severity = warningdotnet_diagnostic.SA1515.severity = warningdotnet_diagnostic.SA1518.severity = warning-
# EditorConfig for MediaBrowser.Model
# https://EditorConfig.org
root = true
# StyleCop Analyzer Rules - Additional suppressions for Emby.Naming
dotnet_diagnostic.SA1200.severity = none
dotnet_diagnostic.SA1633.severity = none
[*]
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
# Reduce severity for trailing commas and comments
[*.cs]
indent_size = 4
indent_style = space
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false
# StyleCop Analyzer Rules - Disabled for project consistency
dotnet_diagnostic.SA1101.severity = none
dotnet_diagnostic.SA1309.severity = none
dotnet_diagnostic.SA1204.severity = none
dotnet_diagnostic.SA1202.severity = none
dotnet_diagnostic.SA1135.severity = none
dotnet_diagnostic.SA1600.severity = none
dotnet_diagnostic.SA1601.severity = none
# StyleCop Analyzer Rules - Reduced severity
dotnet_diagnostic.SA1413.severity = silent
dotnet_diagnostic.SA1515.severity = silent
dotnet_diagnostic.SA1512.severity = silent
dotnet_diagnostic.SA1518.severity = warning
dotnet_diagnostic.SA1648.severity = none
# StyleCop Analyzer Rules - Additional suppressions
dotnet_diagnostic.SA1200.severity = none
dotnet_diagnostic.SA1633.severity = none
# IDE Rules - Suppress non-critical suggestions
dotnet_diagnostic.IDE0008.severity = silent
@@ -17,7 +42,7 @@ dotnet_diagnostic.IDE0051.severity = warning
dotnet_diagnostic.IDE0055.severity = warning
dotnet_diagnostic.IDE0057.severity = silent
dotnet_diagnostic.IDE0058.severity = silent
dotnet_diagnostic.IDE0065.severity = silent
dotnet_diagnostic.IDE0065.severity = none
dotnet_diagnostic.IDE0078.severity = silent
dotnet_diagnostic.IDE0090.severity = silent
dotnet_diagnostic.IDE0160.severity = silent
@@ -33,4 +58,3 @@ dotnet_diagnostic.CA1310.severity = warning
# C# Code Style - Using directive placement
csharp_using_directive_placement = outside_namespace:silent
+4
View File
@@ -8,10 +8,14 @@
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>NU1902;NU1903</WarningsNotAsErrors>
<!-- Suppress IDE0065 globally for all projects -->
<NoWarn>$(NoWarn);IDE0065</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<!-- Don't enforce code style rules as build errors in Debug -->
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
+1
View File
@@ -20,6 +20,7 @@
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningsAsErrors></WarningsAsErrors>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<NoWarn>$(NoWarn);IDE0065</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
+131 -144
View File
@@ -2,178 +2,165 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Emby.Photos;
#pragma warning disable SA1309 // Variables should not begin with underscore
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
using TagLib;
using TagLib.IFD;
using TagLib.IFD.Entries;
using TagLib.IFD.Tags;
/// <summary>
/// Metadata provider for photos.
/// </summary>
public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
namespace Emby.Photos
{
private readonly ILogger<PhotoProvider> _logger;
private readonly IImageProcessor _imageProcessor;
// Other extensions might cause taglib to hang
private readonly string[] _includeExtensions = [".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif"];
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using TagLib;
using TagLib.IFD;
using TagLib.IFD.Entries;
using TagLib.IFD.Tags;
/// <summary>
/// Initializes a new instance of the <see cref="PhotoProvider" /> class.
/// Metadata provider for photos.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="imageProcessor">The image processor.</param>
public PhotoProvider(ILogger<PhotoProvider> logger, IImageProcessor imageProcessor)
public class PhotoProvider(ILogger<PhotoProvider> logger, IImageProcessor imageProcessor) : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
{
_logger = logger;
_imageProcessor = imageProcessor;
}
// Other extensions might cause taglib to hang
private readonly string[] _includeExtensions = [".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif"];
/// <inheritdoc />
public string Name => "Embedded Information";
/// <inheritdoc />
public string Name => "Embedded Information";
/// <inheritdoc />
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{
if (item.IsFileProtocol)
/// <inheritdoc />
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{
var file = directoryService.GetFile(item.Path);
return file is not null && item.HasChanged(file.LastWriteTimeUtc);
if (item.IsFileProtocol)
{
FileSystemMetadata? file = directoryService.GetFile(item.Path);
return file is not null && item.HasChanged(file.LastWriteTimeUtc);
}
return false;
}
return false;
}
/// <inheritdoc />
public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
/// <inheritdoc />
public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
try
{
using var file = TagLib.File.Create(item.Path);
if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
{
var structure = tag.Structure;
if (structure?.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
{
var exifStructure = exif.Structure;
if (exifStructure is not null)
{
if (exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) is RationalIFDEntry apertureEntry)
{
item.Aperture = (double)apertureEntry.Value.Numerator / apertureEntry.Value.Denominator;
}
item.SetImagePath(ImageType.Primary, item.Path);
if (exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) is RationalIFDEntry shutterSpeedEntry)
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
if (this._includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
{
try
{
using TagLib.File file = TagLib.File.Create(item.Path);
if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
{
IFDStructure? structure = tag.Structure;
if (structure?.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
{
IFDStructure? exifStructure = exif.Structure;
if (exifStructure is not null)
{
item.ShutterSpeed = (double)shutterSpeedEntry.Value.Numerator / shutterSpeedEntry.Value.Denominator;
if (exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) is RationalIFDEntry apertureEntry)
{
item.Aperture = (double)apertureEntry.Value.Numerator / apertureEntry.Value.Denominator;
}
if (exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) is RationalIFDEntry shutterSpeedEntry)
{
item.ShutterSpeed = (double)shutterSpeedEntry.Value.Numerator / shutterSpeedEntry.Value.Denominator;
}
}
}
}
}
if (file is TagLib.Image.File image)
if (file is TagLib.Image.File image)
{
item.CameraMake = image.ImageTag.Make;
item.CameraModel = image.ImageTag.Model;
item.Width = image.Properties.PhotoWidth;
item.Height = image.Properties.PhotoHeight;
item.CommunityRating = image.ImageTag.Rating;
item.Overview = image.ImageTag.Comment;
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
&& !item.LockedFields.Contains(MetadataField.Name))
{
item.Name = image.ImageTag.Title;
}
DateTime? dateTaken = image.ImageTag.DateTime;
if (dateTaken.HasValue)
{
item.DateCreated = dateTaken.Value.ToUniversalTime();
item.PremiereDate = dateTaken.Value;
item.ProductionYear = dateTaken.Value.Year;
}
item.Genres = image.ImageTag.Genres;
item.Tags = image.ImageTag.Keywords;
item.Software = image.ImageTag.Software;
if (image.ImageTag.Orientation == TagLib.Image.ImageOrientation.None)
{
item.Orientation = null;
}
else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
{
item.Orientation = orientation;
}
item.ExposureTime = image.ImageTag.ExposureTime;
item.FocalLength = image.ImageTag.FocalLength;
item.Latitude = image.ImageTag.Latitude;
item.Longitude = image.ImageTag.Longitude;
item.Altitude = image.ImageTag.Altitude;
item.IsoSpeedRating = image.ImageTag.ISOSpeedRatings.HasValue
? Convert.ToInt32(image.ImageTag.ISOSpeedRatings.Value)
: null;
}
}
catch (Exception ex)
{
item.CameraMake = image.ImageTag.Make;
item.CameraModel = image.ImageTag.Model;
item.Width = image.Properties.PhotoWidth;
item.Height = image.Properties.PhotoHeight;
item.CommunityRating = image.ImageTag.Rating;
item.Overview = image.ImageTag.Comment;
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
&& !item.LockedFields.Contains(MetadataField.Name))
{
item.Name = image.ImageTag.Title;
}
var dateTaken = image.ImageTag.DateTime;
if (dateTaken.HasValue)
{
item.DateCreated = dateTaken.Value.ToUniversalTime();
item.PremiereDate = dateTaken.Value;
item.ProductionYear = dateTaken.Value.Year;
}
item.Genres = image.ImageTag.Genres;
item.Tags = image.ImageTag.Keywords;
item.Software = image.ImageTag.Software;
if (image.ImageTag.Orientation == TagLib.Image.ImageOrientation.None)
{
item.Orientation = null;
}
else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
{
item.Orientation = orientation;
}
item.ExposureTime = image.ImageTag.ExposureTime;
item.FocalLength = image.ImageTag.FocalLength;
item.Latitude = image.ImageTag.Latitude;
item.Longitude = image.ImageTag.Longitude;
item.Altitude = image.ImageTag.Altitude;
if (image.ImageTag.ISOSpeedRatings.HasValue)
{
item.IsoSpeedRating = Convert.ToInt32(image.ImageTag.ISOSpeedRatings.Value);
}
else
{
item.IsoSpeedRating = null;
}
logger.LogError(ex, "Image Provider - Error reading image tag for {0}", item.Path);
}
}
catch (Exception ex)
if (item.Width <= 0 || item.Height <= 0)
{
_logger.LogError(ex, "Image Provider - Error reading image tag for {0}", item.Path);
}
}
ItemImageInfo img = item.GetImageInfo(ImageType.Primary, 0);
if (item.Width <= 0 || item.Height <= 0)
{
var img = item.GetImageInfo(ImageType.Primary, 0);
try
{
var size = _imageProcessor.GetImageDimensions(item, img);
if (size.Width > 0 && size.Height > 0)
try
{
item.Width = size.Width;
item.Height = size.Height;
ImageDimensions size = imageProcessor.GetImageDimensions(item, img);
if (size.Width > 0 && size.Height > 0)
{
item.Width = size.Width;
item.Height = size.Height;
}
}
catch (ArgumentException)
{
// format not supported
}
}
catch (ArgumentException)
{
// format not supported
}
}
const ItemUpdateType Result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
return Task.FromResult(Result);
const ItemUpdateType Result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
return Task.FromResult(Result);
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "DaMoU2oavX0=",
"dgSpecHash": "leFWYb0Dcsk=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\Emby.Photos\\Emby.Photos.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "CU3WcVn0qXE=",
"dgSpecHash": "ZVA5ePVHqjg=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\Emby.Server.Implementations\\Emby.Server.Implementations.csproj",
"expectedPackageFiles": [
+177
View File
@@ -0,0 +1,177 @@
# How to Ignore IDE0065 Error Message
## What is IDE0065?
**IDE0065**: "Using directives must be placed outside of a namespace declaration"
This is a code style warning about where `using` statements should be placed - inside or outside the namespace.
## Solutions Implemented ✅
### Solution 1: .editorconfig (Applied) ✅
Updated `.editorconfig` to completely disable IDE0065:
```ini
# IDE Rules - Suppress non-critical suggestions
dotnet_diagnostic.IDE0065.severity = none
```
**Severity Levels:**
- `none` = Completely disabled (doesn't appear anywhere)
- `silent` = Hidden in IDE, but may show in build
- `suggestion` = Shows as hint in IDE
- `warning` = Shows as warning
- `error` = Blocks build
### Solution 2: Project File (Applied) ✅
Added to `Emby.Naming.csproj`:
```xml
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<NoWarn>$(NoWarn);IDE0065</NoWarn>
</PropertyGroup>
```
This ensures IDE0065 is completely suppressed during build.
## Additional Methods (If Needed)
### Method 3: Global Suppression File
Create a file `GlobalSuppressions.cs` in your project:
```csharp
// <copyright file="GlobalSuppressions.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0065:Misplaced using directive", Justification = "Project style preference")]
```
### Method 4: Suppress for All Configurations
If you want to suppress IDE0065 for **both Debug and Release**, add this to your `.csproj`:
```xml
<PropertyGroup>
<NoWarn>$(NoWarn);IDE0065</NoWarn>
</PropertyGroup>
```
### Method 5: Visual Studio Settings (Local Only)
If you only want to hide it in **your IDE** (doesn't affect builds):
1. Open Visual Studio
2. **Tools****Options**
3. **Text Editor****C#****Code Style****Formatting**
4. Under "Usings", set your preference
5. Or go to **Options****Environment****Task List** and filter warnings
**Note:** This only affects your local IDE, not team builds.
## Verify the Changes
### Check in Visual Studio:
1. Restart Visual Studio (or reload solution)
2. Build the project
3. IDE0065 should no longer appear in Error List
### Check in Command Line:
```powershell
# Clean and rebuild
dotnet clean Emby.Naming\Emby.Naming.csproj
dotnet build Emby.Naming\Emby.Naming.csproj
# IDE0065 should not appear in output
```
## What the Changes Mean
### For Your IDE:
- ✅ IDE0065 will **not show** in the Error List
- ✅ No underlines or suggestions in the code editor
- ✅ Quick Actions (Ctrl+.) won't offer to "fix" using placement
### For Builds:
- ✅ IDE0065 will **not appear** in build output
- ✅ CI/CD builds won't show this warning
- ✅ Build logs will be cleaner
### For Your Team:
-`.editorconfig` is checked into source control
- ✅ All developers will have the same suppression
- ✅ Consistent experience across the team
## Why Both Methods?
We applied **both** methods for maximum coverage:
1. **`.editorconfig`** → Suppresses in IDE and most build scenarios
2. **`<NoWarn>`** → Guarantees suppression during MSBuild/command-line builds
This ensures IDE0065 is completely eliminated everywhere!
## Other Common IDE Errors to Suppress
If you want to suppress other common warnings, add them to `<NoWarn>`:
```xml
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<NoWarn>$(NoWarn);IDE0065;IDE0008;IDE0058;SA1309;SA1101</NoWarn>
</PropertyGroup>
```
**Common Ones:**
- `IDE0008` = Use explicit type instead of 'var'
- `IDE0058` = Expression value is never used
- `IDE0065` = Using directive placement
- `SA1309` = Field names should not begin with underscore
- `SA1101` = Prefix local calls with 'this'
- `SA1200` = Using directives placement (StyleCop version)
## Testing
After making these changes:
```powershell
# 1. Clean everything
dotnet clean
# 2. Rebuild
dotnet build Emby.Naming\Emby.Naming.csproj
# 3. Check for IDE0065 in output (should be gone!)
```
Expected result: **No IDE0065 warnings**
## Rollback (If Needed)
If you want to re-enable IDE0065 later:
### In .editorconfig:
```ini
dotnet_diagnostic.IDE0065.severity = suggestion
```
### In .csproj:
Remove `IDE0065` from the `<NoWarn>` list.
## Summary
**Both methods applied!**
- `.editorconfig` updated: `IDE0065.severity = none`
- `Emby.Naming.csproj` updated: `<NoWarn>$(NoWarn);IDE0065</NoWarn>`
**Result:** IDE0065 is now completely suppressed in:
- ✅ Visual Studio IDE
- ✅ Command-line builds
- ✅ MSBuild
- ✅ CI/CD pipelines
You should no longer see IDE0065 anywhere! 🎉
+1 -1
View File
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "sEpjYe2J48k=",
"dgSpecHash": "4CpeL+8YFHs=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\Jellyfin.Api\\Jellyfin.Api.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "2uqnqkg9VpI=",
"dgSpecHash": "8rGyZK2RqJs=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\Jellyfin.Server.Implementations\\Jellyfin.Server.Implementations.csproj",
"expectedPackageFiles": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "uvfyRwbCsEk=",
"dgSpecHash": "WNAhlXOd1t0=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\Jellyfin.Server\\Jellyfin.Server.csproj",
"expectedPackageFiles": [
+27
View File
@@ -0,0 +1,27 @@
# EditorConfig for MediaBrowser.Controller
# https://EditorConfig.org
root = true
[*.cs]
# Disable StyleCop rules that are not followed in this legacy project
dotnet_diagnostic.SA1101.severity = none
dotnet_diagnostic.SA1200.severity = none
dotnet_diagnostic.SA1202.severity = none
dotnet_diagnostic.SA1204.severity = none
dotnet_diagnostic.SA1309.severity = none
dotnet_diagnostic.SA1413.severity = none
dotnet_diagnostic.SA1515.severity = none
dotnet_diagnostic.SA1512.severity = none
dotnet_diagnostic.SA1600.severity = none
dotnet_diagnostic.SA1601.severity = none
dotnet_diagnostic.SA1602.severity = none
dotnet_diagnostic.SA1128.severity = none
dotnet_diagnostic.SA1009.severity = none
dotnet_diagnostic.SA1108.severity = none
# Disable IDisposableAnalyzers rules
dotnet_diagnostic.IDISP001.severity = none
dotnet_diagnostic.IDISP003.severity = none
dotnet_diagnostic.IDISP007.severity = none
dotnet_diagnostic.IDISP008.severity = none
@@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.BaseItemManager
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public BaseItemManager(IServerConfigurationManager serverConfigurationManager)
{
_serverConfigurationManager = serverConfigurationManager;
this._serverConfigurationManager = serverConfigurationManager;
}
/// <inheritdoc />
@@ -46,7 +46,7 @@ namespace MediaBrowser.Controller.BaseItemManager
return libraryTypeOptions.MetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
}
var itemConfig = _serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name);
var itemConfig = this._serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name);
return itemConfig is null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
}
@@ -70,7 +70,7 @@ namespace MediaBrowser.Controller.BaseItemManager
return libraryTypeOptions.ImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
}
var itemConfig = _serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name);
var itemConfig = this._serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name);
return itemConfig is null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
}
}
+36 -4
View File
@@ -27,12 +27,18 @@ namespace MediaBrowser.Controller.Channels
[JsonIgnore]
public override SourceType SourceType => SourceType.Channel;
/// <summary>
/// Determines whether this channel is visible to the specified user.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="skipAllowedTagsCheck">Whether to skip the allowed tags check.</param>
/// <returns>True if visible; otherwise, false.</returns>
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
if (blockedChannelsPreference.Length != 0)
{
if (blockedChannelsPreference.Contains(Id))
if (blockedChannelsPreference.Contains(this.Id))
{
return false;
}
@@ -40,7 +46,7 @@ namespace MediaBrowser.Controller.Channels
else
{
if (!user.HasPermission(PermissionKind.EnableAllChannels)
&& !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels).Contains(Id))
&& !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels).Contains(this.Id))
{
return false;
}
@@ -49,12 +55,17 @@ namespace MediaBrowser.Controller.Channels
return base.IsVisible(user, skipAllowedTagsCheck);
}
/// <summary>
/// Gets items from the channel.
/// </summary>
/// <param name="query">The items query.</param>
/// <returns>The query result.</returns>
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
{
try
{
query.Parent = this;
query.ChannelIds = new Guid[] { Id };
query.ChannelIds = new Guid[] { this.Id };
// Don't blow up here because it could cause parent screens with other content to fail
return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
@@ -66,21 +77,42 @@ namespace MediaBrowser.Controller.Channels
}
}
/// <summary>
/// Gets the internal metadata path for this channel.
/// </summary>
/// <param name="basePath">The base path.</param>
/// <returns>The internal metadata path.</returns>
protected override string GetInternalMetadataPath(string basePath)
{
return GetInternalMetadataPath(basePath, Id);
return GetInternalMetadataPath(basePath, this.Id);
}
/// <summary>
/// Gets the internal metadata path for a channel with the specified ID.
/// </summary>
/// <param name="basePath">The base path.</param>
/// <param name="id">The channel ID.</param>
/// <returns>The internal metadata path.</returns>
public static string GetInternalMetadataPath(string basePath, Guid id)
{
return System.IO.Path.Combine(basePath, "channels", id.ToString("N", CultureInfo.InvariantCulture), "metadata");
}
/// <summary>
/// Determines whether this channel can be deleted.
/// </summary>
/// <returns>True if deletable; otherwise, false.</returns>
public override bool CanDelete()
{
return false;
}
/// <summary>
/// Determines whether the channel item is visible to the specified user.
/// </summary>
/// <param name="channelItem">The channel item.</param>
/// <param name="user">The user.</param>
/// <returns>True if visible; otherwise, false.</returns>
internal static bool IsChannelVisible(BaseItem channelItem, User user)
{
var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString(string.Empty));
@@ -142,7 +142,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public SongInfo GetLookupInfo()
{
var info = GetItemLookupInfo<SongInfo>();
var info = this.GetItemLookupInfo<SongInfo>();
info.AlbumArtists = AlbumArtists;
info.Album = Album;
+33 -5
View File
@@ -31,36 +31,64 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public Guid SeriesId { get; set; }
/// <summary>
/// Finds the series sort name.
/// </summary>
/// <returns>The series sort name.</returns>
public string FindSeriesSortName()
{
return SeriesName;
return this.SeriesName;
}
/// <summary>
/// Finds the series name.
/// </summary>
/// <returns>The series name.</returns>
public string FindSeriesName()
{
return SeriesName;
return this.SeriesName;
}
/// <summary>
/// Finds the series presentation unique key.
/// </summary>
/// <returns>The series presentation unique key.</returns>
public string FindSeriesPresentationUniqueKey()
{
return SeriesPresentationUniqueKey;
return this.SeriesPresentationUniqueKey;
}
/// <summary>
/// Gets the default primary image aspect ratio.
/// </summary>
/// <returns>The default primary image aspect ratio.</returns>
public override double GetDefaultPrimaryImageAspectRatio()
{
return 0;
}
/// <summary>
/// Finds the series identifier.
/// </summary>
/// <returns>The series identifier.</returns>
public Guid FindSeriesId()
{
return SeriesId;
return this.SeriesId;
}
/// <summary>
/// Determines whether this audiobook can be downloaded.
/// </summary>
/// <returns>True if downloadable; otherwise, false.</returns>
public override bool CanDownload()
{
return IsFileProtocol;
return this.IsFileProtocol;
}
/// <summary>
/// Gets the unrated type for blocking purposes.
/// </summary>
/// <returns>The unrated item type.</returns>
public override UnratedItem GetBlockUnratedType()
{
return UnratedItem.Book;
@@ -7263,6 +7263,13 @@ namespace MediaBrowser.Controller.MediaEncoding
return inputModifier;
}
/// <summary>
/// Attaches media source information to the encoding job state.
/// </summary>
/// <param name="state">The encoding job state.</param>
/// <param name="encodingOptions">The encoding options.</param>
/// <param name="mediaSource">The media source information.</param>
/// <param name="requestedUrl">The requested URL.</param>
public void AttachMediaSourceInfo(
EncodingJobInfo state,
EncodingOptions encodingOptions,
@@ -7327,26 +7334,26 @@ namespace MediaBrowser.Controller.MediaEncoding
requestedUrl = "test." + videoRequest.Container;
}
videoRequest.VideoCodec = InferVideoCodec(requestedUrl);
videoRequest.VideoCodec = this.InferVideoCodec(requestedUrl);
}
state.VideoStream = GetMediaStream(mediaStreams, videoRequest.VideoStreamIndex, MediaStreamType.Video);
state.SubtitleStream = GetMediaStream(mediaStreams, videoRequest.SubtitleStreamIndex, MediaStreamType.Subtitle, false);
state.VideoStream = this.GetMediaStream(mediaStreams, videoRequest.VideoStreamIndex, MediaStreamType.Video);
state.SubtitleStream = this.GetMediaStream(mediaStreams, videoRequest.SubtitleStreamIndex, MediaStreamType.Subtitle, false);
state.SubtitleDeliveryMethod = videoRequest.SubtitleMethod;
state.AudioStream = GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio);
state.AudioStream = this.GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio);
if (state.SubtitleStream is not null && !state.SubtitleStream.IsExternal)
{
state.InternalSubtitleStreamOffset = mediaStreams.Where(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal).ToList().IndexOf(state.SubtitleStream);
}
EnforceResolutionLimit(state);
this.EnforceResolutionLimit(state);
NormalizeSubtitleEmbed(state);
this.NormalizeSubtitleEmbed(state);
}
else
{
state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
state.AudioStream = this.GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true);
}
state.MediaSource = mediaSource;
@@ -7357,11 +7364,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var supportedAudioCodecsList = supportedAudioCodecs.ToList();
ShiftAudioCodecsIfNeeded(supportedAudioCodecsList, state.AudioStream);
this.ShiftAudioCodecsIfNeeded(supportedAudioCodecsList, state.AudioStream);
state.SupportedAudioCodecs = supportedAudioCodecsList.ToArray();
request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(_mediaEncoder.CanEncodeToAudioCodec)
request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(this._mediaEncoder.CanEncodeToAudioCodec)
?? state.SupportedAudioCodecs.FirstOrDefault();
}
@@ -7370,7 +7377,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var supportedVideoCodecsList = supportedVideoCodecs.ToList();
ShiftVideoCodecsIfNeeded(supportedVideoCodecsList, encodingOptions);
this.ShiftVideoCodecsIfNeeded(supportedVideoCodecsList, encodingOptions);
state.SupportedVideoCodecs = supportedVideoCodecsList.ToArray();
@@ -7791,6 +7798,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|| (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec));
}
/// <summary>
/// Gets the video sync option based on the encoder version.
/// </summary>
/// <param name="videoSync">The video sync parameter.</param>
/// <param name="encoderVersion">The encoder version.</param>
/// <returns>The video sync option string.</returns>
public static string GetVideoSyncOption(string videoSync, Version encoderVersion)
{
if (string.IsNullOrEmpty(videoSync))
@@ -7808,7 +7821,7 @@ namespace MediaBrowser.Controller.MediaEncoding
0 => " -fps_mode passthrough",
1 => " -fps_mode cfr",
2 => " -fps_mode vfr",
_ => string.Empty
_ => string.Empty,
};
}
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "No+LvUzpMog=",
"dgSpecHash": "oIB4cupu3LM=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\MediaBrowser.Controller\\MediaBrowser.Controller.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "MUG8U7vEPzE=",
"dgSpecHash": "GeC+ga1Uo2A=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\MediaBrowser.LocalMetadata\\MediaBrowser.LocalMetadata.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "GTQOs1iNxFU=",
"dgSpecHash": "aQOA3jWvZ/0=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\MediaBrowser.MediaEncoding\\MediaBrowser.MediaEncoding.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "O+ANw3v7loA=",
"dgSpecHash": "ncCtXCalGV4=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\MediaBrowser.Providers\\MediaBrowser.Providers.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "rQazb3gGG1E=",
"dgSpecHash": "P3fDKwImSWA=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\MediaBrowser.XbmcMetadata\\MediaBrowser.XbmcMetadata.csproj",
"expectedPackageFiles": [
@@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Jellyfin.CodeAnalysis")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+477045704e49d02668be2cddb91b218a9e70ec95")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+dbeec2e9f0c8f46c715f999e2be12d779e1fbd62")]
[assembly: System.Reflection.AssemblyProductAttribute("Jellyfin.CodeAnalysis")]
[assembly: System.Reflection.AssemblyTitleAttribute("Jellyfin.CodeAnalysis")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
0aa0b87087f971012539e7d2b22ae3fc0248cc13dafe15e7394876c75cd2b839
9034528098f49c58c86624aecd413bc8526e219cc0c6d56bad4cbb6d5ce48570
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "7p0VjWNAkOM=",
"dgSpecHash": "vHGJFlfns3Y=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\src\\Jellyfin.Database\\Jellyfin.Database.Providers.Sqlite\\Jellyfin.Database.Providers.Sqlite.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "HMW40QmobtY=",
"dgSpecHash": "Fpoz11tqLl0=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\src\\Jellyfin.Drawing.Skia\\Jellyfin.Drawing.Skia.csproj",
"expectedPackageFiles": [
+37 -37
View File
@@ -64,10 +64,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
IImageEncoder imageEncoder,
IServerConfigurationManager config)
{
_logger = logger;
_fileSystem = fileSystem;
_imageEncoder = imageEncoder;
_appPaths = appPaths;
this._logger = logger;
this._fileSystem = fileSystem;
this._imageEncoder = imageEncoder;
this._appPaths = appPaths;
var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
if (semaphoreCount < 1)
@@ -75,10 +75,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
semaphoreCount = Environment.ProcessorCount;
}
_parallelEncodingLimit = new(semaphoreCount);
this._parallelEncodingLimit = new(semaphoreCount);
}
private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images");
private string ResizedImageCachePath => Path.Combine(this._appPaths.ImageCachePath, "resized-images");
/// <inheritdoc />
public IReadOnlyCollection<string> SupportedInputFormats =>
@@ -113,11 +113,11 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
};
/// <inheritdoc />
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
public bool SupportsImageCollageCreation => this._imageEncoder.SupportsImageCollageCreation;
/// <inheritdoc />
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
=> _imageEncoder.SupportedOutputFormats;
=> this._imageEncoder.SupportedOutputFormats;
/// <inheritdoc />
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
@@ -134,12 +134,12 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
}
var mimeType = MimeTypes.GetMimeType(originalImagePath);
if (!_imageEncoder.SupportsImageEncoding)
if (!this._imageEncoder.SupportsImageEncoding)
{
return (originalImagePath, mimeType, dateModified);
}
var supportedImageInfo = await GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
var supportedImageInfo = await this.GetSupportedImage(originalImagePath, dateModified).ConfigureAwait(false);
originalImagePath = supportedImageInfo.Path;
// Original file doesn't exist, or original file is gif.
@@ -179,8 +179,8 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
int quality = options.Quality;
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
string cacheFilePath = GetCacheFilePath(
ImageFormat outputFormat = this.GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
string cacheFilePath = this.GetCacheFilePath(
originalImagePath,
options.Width,
options.Height,
@@ -204,9 +204,9 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
string resultPath;
// Limit number of parallel (more precisely: concurrent) image encodings to prevent a high memory usage
using (await _parallelEncodingLimit.LockAsync().ConfigureAwait(false))
using (await this._parallelEncodingLimit.LockAsync().ConfigureAwait(false))
{
resultPath = _imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
resultPath = this._imageEncoder.EncodeImage(originalImagePath, dateModified, cacheFilePath, autoOrient, orientation, quality, options, outputFormat);
}
if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase))
@@ -215,19 +215,19 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
}
}
return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
return (cacheFilePath, outputFormat.GetMimeType(), this._fileSystem.GetLastWriteTimeUtc(cacheFilePath));
}
catch (Exception ex)
{
// If it fails for whatever reason, return the original image
_logger.LogError(ex, "Error encoding image");
this._logger.LogError(ex, "Error encoding image");
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
}
}
private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency)
{
var serverFormats = GetSupportedImageOutputFormats();
var serverFormats = this.GetSupportedImageOutputFormats();
// Client doesn't care about format, so start with webp if supported
if (serverFormats.Contains(ImageFormat.Webp) && clientSupportedFormats.Contains(ImageFormat.Webp))
@@ -354,7 +354,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
filename.Append(",v=");
filename.Append(Version);
return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension());
return this.GetCachePath(this.ResizedImageCachePath, filename.ToString(), format.GetExtension());
}
/// <inheritdoc />
@@ -369,9 +369,9 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
}
string path = info.Path;
_logger.LogDebug("Getting image size for item {ItemType} {Path}", item.GetType().Name, path);
this._logger.LogDebug("Getting image size for item {ItemType} {Path}", item.GetType().Name, path);
ImageDimensions size = GetImageDimensions(path);
ImageDimensions size = this.GetImageDimensions(path);
info.Width = size.Width;
info.Height = size.Height;
@@ -380,13 +380,13 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <inheritdoc />
public ImageDimensions GetImageDimensions(string path)
=> _imageEncoder.GetImageSize(path);
=> this._imageEncoder.GetImageSize(path);
/// <inheritdoc />
public string GetImageBlurHash(string path)
{
var size = GetImageDimensions(path);
return GetImageBlurHash(path, size);
var size = this.GetImageDimensions(path);
return this.GetImageBlurHash(path, size);
}
/// <inheritdoc />
@@ -406,7 +406,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
int xComp = Math.Min((int)xCompF + 1, 9);
int yComp = Math.Min((int)yCompF + 1, 9);
return _imageEncoder.GetImageBlurHash(xComp, yComp, path);
return this._imageEncoder.GetImageBlurHash(xComp, yComp, path);
}
/// <inheritdoc />
@@ -415,11 +415,11 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
=> GetImageCacheTag(item.Path, image.DateModified);
=> this.GetImageCacheTag(item.Path, image.DateModified);
/// <inheritdoc />
public string GetImageCacheTag(BaseItemDto item, ItemImageInfo image)
=> GetImageCacheTag(item.Path, image.DateModified);
=> this.GetImageCacheTag(item.Path, image.DateModified);
/// <inheritdoc />
public string? GetImageCacheTag(BaseItemDto item, ChapterInfo chapter)
@@ -429,7 +429,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return null;
}
return GetImageCacheTag(item.Path, chapter.ImageDateModified);
return this.GetImageCacheTag(item.Path, chapter.ImageDateModified);
}
/// <inheritdoc />
@@ -440,7 +440,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return null;
}
return GetImageCacheTag(item, new ItemImageInfo
return this.GetImageCacheTag(item, new ItemImageInfo
{
Path = chapter.ImagePath,
Type = ImageType.Chapter,
@@ -456,7 +456,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return null;
}
return GetImageCacheTag(user.ProfileImage.Path, user.ProfileImage.LastModified);
return this.GetImageCacheTag(user.ProfileImage.Path, user.ProfileImage.LastModified);
}
private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
@@ -494,7 +494,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
var filename = uniqueName.GetMD5() + fileExtension;
return GetCachePath(path, filename);
return this.GetCachePath(path, filename);
}
/// <summary>
@@ -528,28 +528,28 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
_logger.LogDebug("Creating image collage and saving to {Path}", options.OutputPath);
this._logger.LogDebug("Creating image collage and saving to {Path}", options.OutputPath);
_imageEncoder.CreateImageCollage(options, libraryName);
this._imageEncoder.CreateImageCollage(options, libraryName);
_logger.LogDebug("Completed creation of image collage and saved to {Path}", options.OutputPath);
this._logger.LogDebug("Completed creation of image collage and saved to {Path}", options.OutputPath);
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
if (this._disposed)
{
return;
}
if (_imageEncoder is IDisposable disposable)
if (this._imageEncoder is IDisposable disposable)
{
disposable.Dispose();
}
_parallelEncodingLimit?.Dispose();
this._parallelEncodingLimit?.Dispose();
_disposed = true;
this._disposed = true;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "kfdiYX/qdMo=",
"dgSpecHash": "Vcdm+k15Tts=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\src\\Jellyfin.Drawing\\Jellyfin.Drawing.csproj",
"expectedPackageFiles": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "Wq1Uahcka68=",
"dgSpecHash": "9I/zz4fLYTY=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\src\\Jellyfin.LiveTv\\Jellyfin.LiveTv.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "2kUNnZdwGG8=",
"dgSpecHash": "svddOISkg9E=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\src\\Jellyfin.MediaEncoding.Hls\\Jellyfin.MediaEncoding.Hls.csproj",
"expectedPackageFiles": [
@@ -76,60 +76,62 @@ public static class FfProbeKeyframeExtractor
}
}
/// <summary>
/// Parses the keyframe data from the FFProbe stream.
/// </summary>
/// <param name="reader">The stream reader containing FFProbe output.</param>
/// <returns>The parsed keyframe data.</returns>
internal static KeyframeData ParseStream(StreamReader reader)
{
var keyframes = new List<long>();
double streamDuration = 0;
double formatDuration = 0;
using (reader)
while (!reader.EndOfStream)
{
while (!reader.EndOfStream)
var line = reader.ReadLine().AsSpan();
if (line.IsEmpty)
{
var line = reader.ReadLine().AsSpan();
if (line.IsEmpty)
{
continue;
}
continue;
}
var firstComma = line.IndexOf(',');
var lineType = line[..firstComma];
var rest = line[(firstComma + 1)..];
if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase))
var firstComma = line.IndexOf(',');
var lineType = line[..firstComma];
var rest = line[(firstComma + 1)..];
if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase))
{
// Split time and flags from the packet line. Example line: packet,7169.079000,K_
var secondComma = rest.IndexOf(',');
var ptsTime = rest[..secondComma];
var flags = rest[(secondComma + 1)..];
if (flags.StartsWith("K_"))
{
// Split time and flags from the packet line. Example line: packet,7169.079000,K_
var secondComma = rest.IndexOf(',');
var ptsTime = rest[..secondComma];
var flags = rest[(secondComma + 1)..];
if (flags.StartsWith("K_"))
if (double.TryParse(ptsTime, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var keyframe))
{
if (double.TryParse(ptsTime, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var keyframe))
{
// Have to manually convert to ticks to avoid rounding errors as TimeSpan is only precise down to 1 ms when converting double.
keyframes.Add(Convert.ToInt64(keyframe * TimeSpan.TicksPerSecond));
}
}
}
else if (lineType.Equals("stream", StringComparison.OrdinalIgnoreCase))
{
if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var streamDurationResult))
{
streamDuration = streamDurationResult;
}
}
else if (lineType.Equals("format", StringComparison.OrdinalIgnoreCase))
{
if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var formatDurationResult))
{
formatDuration = formatDurationResult;
// Have to manually convert to ticks to avoid rounding errors as TimeSpan is only precise down to 1 ms when converting double.
keyframes.Add(Convert.ToInt64(keyframe * TimeSpan.TicksPerSecond));
}
}
}
// Prefer the stream duration as it should be more accurate
var duration = streamDuration > 0 ? streamDuration : formatDuration;
return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes);
else if (lineType.Equals("stream", StringComparison.OrdinalIgnoreCase))
{
if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var streamDurationResult))
{
streamDuration = streamDurationResult;
}
}
else if (lineType.Equals("format", StringComparison.OrdinalIgnoreCase))
{
if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var formatDurationResult))
{
formatDuration = formatDurationResult;
}
}
}
// Prefer the stream duration as it should be more accurate
var duration = streamDuration > 0 ? streamDuration : formatDuration;
return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes);
}
}
@@ -18,8 +18,8 @@ public class KeyframeData
/// <param name="keyframeTicks">The video keyframes in ticks.</param>
public KeyframeData(long totalDuration, IReadOnlyList<long> keyframeTicks)
{
TotalDuration = totalDuration;
KeyframeTicks = keyframeTicks;
this.TotalDuration = totalDuration;
this.KeyframeTicks = keyframeTicks;
}
/// <summary>
@@ -70,6 +70,7 @@ internal static class EbmlReaderExtensions
long? tracksPosition = null;
long? cuesPosition = null;
long? infoPosition = null;
// The first element should be a SeekHead otherwise we'll have to search manually
if (!reader.FindElement(MatroskaConstants.SeekHead))
{
@@ -128,6 +129,7 @@ internal static class EbmlReaderExtensions
double? duration = null;
reader.EnterContainer();
// Mandatory element
reader.FindElement(MatroskaConstants.TimestampScale);
var timestampScale = reader.ReadUInt();
@@ -158,6 +160,7 @@ internal static class EbmlReaderExtensions
while (reader.FindElement(MatroskaConstants.TrackEntry))
{
reader.EnterContainer();
// Mandatory element
reader.FindElement(MatroskaConstants.TrackNumber);
var trackNumber = reader.ReadUInt();
@@ -9,26 +9,88 @@ namespace Jellyfin.MediaEncoding.Keyframes.Matroska;
/// </summary>
public static class MatroskaConstants
{
/// <summary>
/// The Matroska segment container identifier.
/// </summary>
internal const ulong SegmentContainer = 0x18538067;
/// <summary>
/// The Matroska seek head identifier.
/// </summary>
internal const ulong SeekHead = 0x114D9B74;
/// <summary>
/// The Matroska seek identifier.
/// </summary>
internal const ulong Seek = 0x4DBB;
/// <summary>
/// The Matroska info identifier.
/// </summary>
internal const ulong Info = 0x1549A966;
/// <summary>
/// The Matroska timestamp scale identifier.
/// </summary>
internal const ulong TimestampScale = 0x2AD7B1;
/// <summary>
/// The Matroska duration identifier.
/// </summary>
internal const ulong Duration = 0x4489;
/// <summary>
/// The Matroska tracks identifier.
/// </summary>
internal const ulong Tracks = 0x1654AE6B;
/// <summary>
/// The Matroska track entry identifier.
/// </summary>
internal const ulong TrackEntry = 0xAE;
/// <summary>
/// The Matroska track number identifier.
/// </summary>
internal const ulong TrackNumber = 0xD7;
/// <summary>
/// The Matroska track type identifier.
/// </summary>
internal const ulong TrackType = 0x83;
/// <summary>
/// The Matroska video track type value.
/// </summary>
internal const ulong TrackTypeVideo = 0x1;
/// <summary>
/// The Matroska subtitle track type value.
/// </summary>
internal const ulong TrackTypeSubtitle = 0x11;
/// <summary>
/// The Matroska cues identifier.
/// </summary>
internal const ulong Cues = 0x1C53BB6B;
/// <summary>
/// The Matroska cue time identifier.
/// </summary>
internal const ulong CueTime = 0xB3;
/// <summary>
/// The Matroska cue point identifier.
/// </summary>
internal const ulong CuePoint = 0xBB;
/// <summary>
/// The Matroska cue track positions identifier.
/// </summary>
internal const ulong CueTrackPositions = 0xB7;
/// <summary>
/// The Matroska cue point track number identifier.
/// </summary>
internal const ulong CuePointTrackNumber = 0xF7;
}
@@ -27,6 +27,7 @@ public static class MatroskaKeyframeExtractor
using var reader = new EbmlReader(stream);
var seekHead = reader.ReadSeekHead();
// External lib does not support seeking backwards (yet)
Info info;
ulong videoTrackNumber;
@@ -49,6 +50,7 @@ public static class MatroskaKeyframeExtractor
{
reader.EnterContainer();
ulong? trackNumber = null;
// Mandatory element
reader.FindElement(MatroskaConstants.CueTime);
var cueTime = reader.ReadUInt();
@@ -16,8 +16,8 @@ internal class Info
/// <param name="duration">The duration of the entire file.</param>
public Info(long timestampScale, double? duration)
{
TimestampScale = timestampScale;
Duration = duration;
this.TimestampScale = timestampScale;
this.Duration = duration;
}
/// <summary>
@@ -17,9 +17,9 @@ internal class SeekHead
/// <param name="cuesPosition">The relative file position of the cues segment.</param>
public SeekHead(long infoPosition, long tracksPosition, long cuesPosition)
{
InfoPosition = infoPosition;
TracksPosition = tracksPosition;
CuesPosition = cuesPosition;
this.InfoPosition = infoPosition;
this.TracksPosition = tracksPosition;
this.CuesPosition = cuesPosition;
}
/// <summary>
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "mTVFQQSIGo8=",
"dgSpecHash": "fWFpFkhudto=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\src\\Jellyfin.MediaEncoding.Keyframes\\Jellyfin.MediaEncoding.Keyframes.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "o8ef9/49oOU=",
"dgSpecHash": "nYThmTXT+N4=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\src\\Jellyfin.Networking\\Jellyfin.Networking.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "I6cvgut0KE0=",
"dgSpecHash": "jz3od75Vj9o=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\tests\\Jellyfin.Api.Tests\\Jellyfin.Api.Tests.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "rSTC0c4hKeg=",
"dgSpecHash": "D3+az8yIj/g=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\tests\\Jellyfin.Common.Tests\\Jellyfin.Common.Tests.csproj",
"expectedPackageFiles": [
@@ -13,8 +13,17 @@ namespace Jellyfin.Controller.Tests
using Moq;
using Xunit;
/// <summary>
/// Tests for <see cref="BaseItemManager"/>.
/// </summary>
public class BaseItemManagerTests
{
/// <summary>
/// Tests that IsMetadataFetcherEnabled checks library and server configuration options.
/// </summary>
/// <param name="itemType">The type of item to test.</param>
/// <param name="fetcherName">The name of the metadata fetcher.</param>
/// <param name="expected">The expected result.</param>
[Theory]
[InlineData(typeof(Book), "LibraryEnabled", true)]
[InlineData(typeof(Book), "LibraryDisabled", false)]
@@ -24,30 +33,36 @@ namespace Jellyfin.Controller.Tests
{
BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!;
var libraryTypeOptions = itemType == typeof(Book)
TypeOptions? libraryTypeOptions = itemType == typeof(Book)
? new TypeOptions
{
Type = "Book",
MetadataFetchers = new[] { "LibraryEnabled" }
MetadataFetchers = ["LibraryEnabled"],
}
: null;
var serverConfiguration = new ServerConfiguration();
foreach (var typeConfig in serverConfiguration.MetadataOptions)
ServerConfiguration serverConfiguration = new();
foreach (MetadataOptions typeConfig in serverConfiguration.MetadataOptions)
{
typeConfig.DisabledMetadataFetchers = new[] { "ServerDisabled" };
typeConfig.DisabledMetadataFetchers = ["ServerDisabled"];
}
var serverConfigurationManager = new Mock<IServerConfigurationManager>();
serverConfigurationManager.Setup(scm => scm.Configuration)
Mock<IServerConfigurationManager> serverConfigurationManager = new();
_ = serverConfigurationManager.Setup(scm => scm.Configuration)
.Returns(serverConfiguration);
var baseItemManager = new BaseItemManager(serverConfigurationManager.Object);
var actual = baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, fetcherName);
BaseItemManager baseItemManager = new(serverConfigurationManager.Object);
bool actual = baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, fetcherName);
Assert.Equal(expected, actual);
}
/// <summary>
/// Tests that IsImageFetcherEnabled checks library and server configuration options.
/// </summary>
/// <param name="itemType">The type of item to test.</param>
/// <param name="fetcherName">The name of the image fetcher.</param>
/// <param name="expected">The expected result.</param>
[Theory]
[InlineData(typeof(Book), "LibraryEnabled", true)]
[InlineData(typeof(Book), "LibraryDisabled", false)]
@@ -57,26 +72,26 @@ namespace Jellyfin.Controller.Tests
{
BaseItem item = (BaseItem)Activator.CreateInstance(itemType)!;
var libraryTypeOptions = itemType == typeof(Book)
TypeOptions? libraryTypeOptions = itemType == typeof(Book)
? new TypeOptions
{
Type = "Book",
ImageFetchers = new[] { "LibraryEnabled" }
ImageFetchers = ["LibraryEnabled"],
}
: null;
var serverConfiguration = new ServerConfiguration();
foreach (var typeConfig in serverConfiguration.MetadataOptions)
ServerConfiguration serverConfiguration = new();
foreach (MetadataOptions typeConfig in serverConfiguration.MetadataOptions)
{
typeConfig.DisabledImageFetchers = new[] { "ServerDisabled" };
typeConfig.DisabledImageFetchers = ["ServerDisabled"];
}
var serverConfigurationManager = new Mock<IServerConfigurationManager>();
serverConfigurationManager.Setup(scm => scm.Configuration)
Mock<IServerConfigurationManager> serverConfigurationManager = new();
_ = serverConfigurationManager.Setup(scm => scm.Configuration)
.Returns(serverConfiguration);
var baseItemManager = new BaseItemManager(serverConfigurationManager.Object);
var actual = baseItemManager.IsImageFetcherEnabled(item, libraryTypeOptions, fetcherName);
BaseItemManager baseItemManager = new(serverConfigurationManager.Object);
bool actual = baseItemManager.IsImageFetcherEnabled(item, libraryTypeOptions, fetcherName);
Assert.Equal(expected, actual);
}
@@ -4,124 +4,140 @@
namespace Jellyfin.Controller.Tests
{
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Moq;
using Xunit;
/// <summary>
/// Tests for <see cref="DirectoryService"/>.
/// </summary>
public class DirectoryServiceTests
{
private const string LowerCasePath = "/music/someartist";
private const string UpperCasePath = "/music/SOMEARTIST";
private static readonly FileSystemMetadata[] _lowerCaseFileSystemMetadata =
{
private static readonly FileSystemMetadata[] LowerCaseFileSystemMetadata =
[
new()
{
FullName = LowerCasePath + "/Artwork",
IsDirectory = true
IsDirectory = true,
},
new()
{
FullName = LowerCasePath + "/Some Other Folder",
IsDirectory = true
IsDirectory = true,
},
new()
{
FullName = LowerCasePath + "/Song 2.mp3",
IsDirectory = false
IsDirectory = false,
},
new()
{
FullName = LowerCasePath + "/Song 3.mp3",
IsDirectory = false
}
};
IsDirectory = false,
},
];
private static readonly FileSystemMetadata[] _upperCaseFileSystemMetadata =
{
private static readonly FileSystemMetadata[] UpperCaseFileSystemMetadata =
[
new()
{
FullName = UpperCasePath + "/Lyrics",
IsDirectory = true
IsDirectory = true,
},
new()
{
FullName = UpperCasePath + "/Song 1.mp3",
IsDirectory = false
}
};
IsDirectory = false,
},
];
/// <summary>
/// Tests that GetFileSystemEntries caches entries for paths with different casing.
/// </summary>
[Fact]
public void GetFileSystemEntries_GivenPathsWithDifferentCasing_CachesAll()
{
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object);
Mock<IFileSystem> fileSystemMock = new();
_ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(UpperCaseFileSystemMetadata);
_ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(LowerCaseFileSystemMetadata);
DirectoryService directoryService = new(fileSystemMock.Object);
var upperCaseResult = directoryService.GetFileSystemEntries(UpperCasePath);
var lowerCaseResult = directoryService.GetFileSystemEntries(LowerCasePath);
FileSystemMetadata[] upperCaseResult = directoryService.GetFileSystemEntries(UpperCasePath);
FileSystemMetadata[] lowerCaseResult = directoryService.GetFileSystemEntries(LowerCasePath);
Assert.Equal(_upperCaseFileSystemMetadata, upperCaseResult);
Assert.Equal(_lowerCaseFileSystemMetadata, lowerCaseResult);
Assert.Equal(UpperCaseFileSystemMetadata, upperCaseResult);
Assert.Equal(LowerCaseFileSystemMetadata, lowerCaseResult);
}
/// <summary>
/// Tests that GetFiles returns correct files for paths with different casing.
/// </summary>
[Fact]
public void GetFiles_GivenPathsWithDifferentCasing_ReturnsCorrectFiles()
{
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object);
Mock<IFileSystem> fileSystemMock = new();
_ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(UpperCaseFileSystemMetadata);
_ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(LowerCaseFileSystemMetadata);
DirectoryService directoryService = new(fileSystemMock.Object);
var upperCaseResult = directoryService.GetFiles(UpperCasePath);
var lowerCaseResult = directoryService.GetFiles(LowerCasePath);
List<FileSystemMetadata> upperCaseResult = directoryService.GetFiles(UpperCasePath);
List<FileSystemMetadata> lowerCaseResult = directoryService.GetFiles(LowerCasePath);
Assert.Equal(_upperCaseFileSystemMetadata.Where(f => !f.IsDirectory), upperCaseResult);
Assert.Equal(_lowerCaseFileSystemMetadata.Where(f => !f.IsDirectory), lowerCaseResult);
Assert.Equal(UpperCaseFileSystemMetadata.Where(f => !f.IsDirectory), upperCaseResult);
Assert.Equal(LowerCaseFileSystemMetadata.Where(f => !f.IsDirectory), lowerCaseResult);
}
/// <summary>
/// Tests that GetDirectories returns correct directories for paths with different casing.
/// </summary>
[Fact]
public void GetDirectories_GivenPathsWithDifferentCasing_ReturnsCorrectDirectories()
{
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(_upperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(_lowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object);
Mock<IFileSystem> fileSystemMock = new();
_ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == UpperCasePath), false)).Returns(UpperCaseFileSystemMetadata);
_ = fileSystemMock.Setup(f => f.GetFileSystemEntries(It.Is<string>(x => x == LowerCasePath), false)).Returns(LowerCaseFileSystemMetadata);
DirectoryService directoryService = new(fileSystemMock.Object);
var upperCaseResult = directoryService.GetDirectories(UpperCasePath);
var lowerCaseResult = directoryService.GetDirectories(LowerCasePath);
List<FileSystemMetadata> upperCaseResult = directoryService.GetDirectories(UpperCasePath);
List<FileSystemMetadata> lowerCaseResult = directoryService.GetDirectories(LowerCasePath);
Assert.Equal(_upperCaseFileSystemMetadata.Where(f => f.IsDirectory), upperCaseResult);
Assert.Equal(_lowerCaseFileSystemMetadata.Where(f => f.IsDirectory), lowerCaseResult);
Assert.Equal(UpperCaseFileSystemMetadata.Where(f => f.IsDirectory), upperCaseResult);
Assert.Equal(LowerCaseFileSystemMetadata.Where(f => f.IsDirectory), lowerCaseResult);
}
/// <summary>
/// Tests that GetFile returns correct file for paths with different casing.
/// </summary>
[Fact]
public void GetFile_GivenFilePathsWithDifferentCasing_ReturnsCorrectFile()
{
const string lowerCasePath = "/music/someartist/song 1.mp3";
var lowerCaseFileSystemMetadata = new FileSystemMetadata
FileSystemMetadata lowerCaseFileSystemMetadata = new()
{
FullName = lowerCasePath,
Exists = true
Exists = true,
};
const string upperCasePath = "/music/SOMEARTIST/SONG 1.mp3";
var upperCaseFileSystemMetadata = new FileSystemMetadata
FileSystemMetadata upperCaseFileSystemMetadata = new()
{
FullName = upperCasePath,
Exists = false
Exists = false,
};
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == upperCasePath))).Returns(upperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == lowerCasePath))).Returns(lowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object);
Mock<IFileSystem> fileSystemMock = new();
_ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == upperCasePath))).Returns(upperCaseFileSystemMetadata);
_ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == lowerCasePath))).Returns(lowerCaseFileSystemMetadata);
DirectoryService directoryService = new(fileSystemMock.Object);
var lowerCaseDirResult = directoryService.GetDirectory(lowerCasePath);
var lowerCaseFileResult = directoryService.GetFile(lowerCasePath);
var upperCaseDirResult = directoryService.GetDirectory(upperCasePath);
var upperCaseFileResult = directoryService.GetFile(upperCasePath);
FileSystemMetadata? lowerCaseDirResult = directoryService.GetDirectory(lowerCasePath);
FileSystemMetadata? lowerCaseFileResult = directoryService.GetFile(lowerCasePath);
FileSystemMetadata? upperCaseDirResult = directoryService.GetDirectory(upperCasePath);
FileSystemMetadata? upperCaseFileResult = directoryService.GetFile(upperCasePath);
Assert.Null(lowerCaseDirResult);
Assert.Equal(lowerCaseFileSystemMetadata, lowerCaseFileResult);
@@ -129,32 +145,35 @@ namespace Jellyfin.Controller.Tests
Assert.Null(upperCaseFileResult);
}
/// <summary>
/// Tests that GetDirectory returns correct directory for paths with different casing.
/// </summary>
[Fact]
public void GetDirectory_GivenFilePathsWithDifferentCasing_ReturnsCorrectDirectory()
{
const string lowerCasePath = "/music/someartist/Lyrics";
var lowerCaseFileSystemMetadata = new FileSystemMetadata
FileSystemMetadata lowerCaseFileSystemMetadata = new()
{
FullName = lowerCasePath,
IsDirectory = true,
Exists = true
Exists = true,
};
const string upperCasePath = "/music/SOMEARTIST/LYRICS";
var upperCaseFileSystemMetadata = new FileSystemMetadata
FileSystemMetadata upperCaseFileSystemMetadata = new()
{
FullName = upperCasePath,
IsDirectory = true,
Exists = false
Exists = false,
};
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == upperCasePath))).Returns(upperCaseFileSystemMetadata);
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == lowerCasePath))).Returns(lowerCaseFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object);
Mock<IFileSystem> fileSystemMock = new();
_ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == upperCasePath))).Returns(upperCaseFileSystemMetadata);
_ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == lowerCasePath))).Returns(lowerCaseFileSystemMetadata);
DirectoryService directoryService = new(fileSystemMock.Object);
var lowerCaseDirResult = directoryService.GetDirectory(lowerCasePath);
var lowerCaseFileResult = directoryService.GetFile(lowerCasePath);
var upperCaseDirResult = directoryService.GetDirectory(upperCasePath);
var upperCaseFileResult = directoryService.GetFile(upperCasePath);
FileSystemMetadata? lowerCaseDirResult = directoryService.GetDirectory(lowerCasePath);
FileSystemMetadata? lowerCaseFileResult = directoryService.GetFile(lowerCasePath);
FileSystemMetadata? upperCaseDirResult = directoryService.GetDirectory(upperCasePath);
FileSystemMetadata? upperCaseFileResult = directoryService.GetFile(upperCasePath);
Assert.Equal(lowerCaseFileSystemMetadata, lowerCaseDirResult);
Assert.Null(lowerCaseFileResult);
@@ -162,92 +181,101 @@ namespace Jellyfin.Controller.Tests
Assert.Null(upperCaseFileResult);
}
/// <summary>
/// Tests that GetFile returns cached file when the path is already cached.
/// </summary>
[Fact]
public void GetFile_GivenCachedPath_ReturnsCachedFile()
{
const string path = "/music/someartist/song 1.mp3";
var cachedFileSystemMetadata = new FileSystemMetadata
FileSystemMetadata cachedFileSystemMetadata = new()
{
FullName = path,
Exists = true
Exists = true,
};
var newFileSystemMetadata = new FileSystemMetadata
FileSystemMetadata newFileSystemMetadata = new()
{
FullName = "/music/SOMEARTIST/song 1.mp3",
Exists = true
Exists = true,
};
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == path))).Returns(cachedFileSystemMetadata);
var directoryService = new DirectoryService(fileSystemMock.Object);
Mock<IFileSystem> fileSystemMock = new();
_ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == path))).Returns(cachedFileSystemMetadata);
DirectoryService directoryService = new(fileSystemMock.Object);
var result = directoryService.GetFile(path);
fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == path))).Returns(newFileSystemMetadata);
var secondResult = directoryService.GetFile(path);
FileSystemMetadata? result = directoryService.GetFile(path);
_ = fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is<string>(x => x == path))).Returns(newFileSystemMetadata);
FileSystemMetadata? secondResult = directoryService.GetFile(path);
Assert.Equivalent(cachedFileSystemMetadata, result);
Assert.Equivalent(cachedFileSystemMetadata, secondResult);
}
/// <summary>
/// Tests that GetFilePaths returns only cached paths when clear is not called.
/// </summary>
[Fact]
public void GetFilePaths_GivenCachedFilePathWithoutClear_ReturnsOnlyCachedPaths()
{
const string path = "/music/someartist";
var cachedPaths = new[]
{
string[] cachedPaths =
[
"/music/someartist/song 1.mp3",
"/music/someartist/song 2.mp3",
"/music/someartist/song 3.mp3",
"/music/someartist/song 4.mp3",
};
var newPaths = new[]
{
];
string[] newPaths =
[
"/music/someartist/song 5.mp3",
"/music/someartist/song 6.mp3",
"/music/someartist/song 7.mp3",
"/music/someartist/song 8.mp3",
};
];
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
var directoryService = new DirectoryService(fileSystemMock.Object);
Mock<IFileSystem> fileSystemMock = new();
_ = fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
DirectoryService directoryService = new(fileSystemMock.Object);
var result = directoryService.GetFilePaths(path);
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
var secondResult = directoryService.GetFilePaths(path);
IReadOnlyList<string> result = directoryService.GetFilePaths(path);
_ = fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
IReadOnlyList<string> secondResult = directoryService.GetFilePaths(path);
Assert.Equal(cachedPaths, result);
Assert.Equal(cachedPaths, secondResult);
}
/// <summary>
/// Tests that GetFilePaths returns new paths when clear is called.
/// </summary>
[Fact]
public void GetFilePaths_GivenCachedFilePathWithClear_ReturnsNewPaths()
{
const string path = "/music/someartist";
var cachedPaths = new[]
{
string[] cachedPaths =
[
"/music/someartist/song 1.mp3",
"/music/someartist/song 2.mp3",
"/music/someartist/song 3.mp3",
"/music/someartist/song 4.mp3",
};
var newPaths = new[]
{
];
string[] newPaths =
[
"/music/someartist/song 5.mp3",
"/music/someartist/song 6.mp3",
"/music/someartist/song 7.mp3",
"/music/someartist/song 8.mp3",
};
];
var fileSystemMock = new Mock<IFileSystem>();
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
var directoryService = new DirectoryService(fileSystemMock.Object);
Mock<IFileSystem> fileSystemMock = new();
_ = fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(cachedPaths);
DirectoryService directoryService = new(fileSystemMock.Object);
var result = directoryService.GetFilePaths(path);
fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
var secondResult = directoryService.GetFilePaths(path, true);
IReadOnlyList<string> result = directoryService.GetFilePaths(path);
_ = fileSystemMock.Setup(f => f.GetFilePaths(It.Is<string>(x => x == path), false)).Returns(newPaths);
IReadOnlyList<string> secondResult = directoryService.GetFilePaths(path, true);
Assert.Equal(cachedPaths, result);
Assert.Equal(newPaths, secondResult);
@@ -2,49 +2,67 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Controller.Tests.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.MediaInfo;
using Moq;
using Xunit;
public class BaseItemTests
namespace Jellyfin.Controller.Tests.Entities
{
[Theory]
[InlineData("", "")]
[InlineData("1", "0000000001")]
[InlineData("t", "t")]
[InlineData("test", "test")]
[InlineData("test1", "test0000000001")]
[InlineData("1test 2", "0000000001test 0000000002")]
public void BaseItem_ModifySortChunks_Valid(string input, string expected)
=> Assert.Equal(expected, BaseItem.ModifySortChunks(input));
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.MediaInfo;
using Moq;
using Xunit;
[Theory]
[InlineData("/Movies/Ted/Ted.mp4", "/Movies/Ted/Ted - Unrated Edition.mp4", "Ted", "Unrated Edition")]
[InlineData("/Movies/Deadpool 2 (2018)/Deadpool 2 (2018).mkv", "/Movies/Deadpool 2 (2018)/Deadpool 2 (2018) - Super Duper Cut.mkv", "Deadpool 2 (2018)", "Super Duper Cut")]
public void GetMediaSourceName_Valid(string primaryPath, string altPath, string name, string altName)
/// <summary>
/// Tests for <see cref="BaseItem"/>.
/// </summary>
public class BaseItemTests
{
var mediaSourceManager = new Mock<IMediaSourceManager>();
mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>()))
.Returns((string x) => MediaProtocol.File);
BaseItem.MediaSourceManager = mediaSourceManager.Object;
var video = new Video()
/// <summary>
/// Tests that ModifySortChunks correctly modifies sort chunks.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="expected">The expected output string.</param>
[Theory]
[InlineData("", "")]
[InlineData("1", "0000000001")]
[InlineData("t", "t")]
[InlineData("test", "test")]
[InlineData("test1", "test0000000001")]
[InlineData("1test 2", "0000000001test 0000000002")]
public void BaseItem_ModifySortChunks_Valid(string input, string expected)
{
Path = primaryPath
};
Assert.Equal(expected, BaseItem.ModifySortChunks(input));
}
var videoAlt = new Video()
/// <summary>
/// Tests that GetMediaSourceName returns the correct name for media sources.
/// </summary>
/// <param name="primaryPath">The primary path.</param>
/// <param name="altPath">The alternate path.</param>
/// <param name="name">The expected name for the primary path.</param>
/// <param name="altName">The expected name for the alternate path.</param>
[Theory]
[InlineData("/Movies/Ted/Ted.mp4", "/Movies/Ted/Ted - Unrated Edition.mp4", "Ted", "Unrated Edition")]
[InlineData("/Movies/Deadpool 2 (2018)/Deadpool 2 (2018).mkv", "/Movies/Deadpool 2 (2018)/Deadpool 2 (2018) - Super Duper Cut.mkv", "Deadpool 2 (2018)", "Super Duper Cut")]
public void GetMediaSourceName_Valid(string primaryPath, string altPath, string name, string altName)
{
Path = altPath,
};
Mock<IMediaSourceManager> mediaSourceManager = new();
_ = mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>()))
.Returns((string x) => MediaProtocol.File);
BaseItem.MediaSourceManager = mediaSourceManager.Object;
video.LocalAlternateVersions = [videoAlt.Path];
Video video = new()
{
Path = primaryPath,
};
Assert.Equal(name, video.GetMediaSourceName(video));
Assert.Equal(altName, video.GetMediaSourceName(videoAlt));
Video videoAlt = new()
{
Path = altPath,
};
video.LocalAlternateVersions = [videoAlt.Path];
Assert.Equal(name, video.GetMediaSourceName(video));
Assert.Equal(altName, video.GetMediaSourceName(videoAlt));
}
}
}
@@ -4,6 +4,7 @@
<PropertyGroup>
<ProjectGuid>{462584F7-5023-4019-9EAC-B98CA458C0A0}</ProjectGuid>
<TargetFramework>net11.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "rCzbZx73oRU=",
"dgSpecHash": "GDKjP2XUxGM=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\tests\\Jellyfin.Controller.Tests\\Jellyfin.Controller.Tests.csproj",
"expectedPackageFiles": [
@@ -8,19 +8,33 @@ namespace Jellyfin.Extensions.Tests
using System.Collections.Generic;
using Xunit;
/// <summary>
/// Tests for CopyTo extension methods.
/// </summary>
public static class CopyToExtensionsTests
{
/// <summary>
/// Gets test data for valid CopyTo operations.
/// </summary>
/// <returns>The test data.</returns>
public static TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>> CopyTo_Valid_Correct_TestData()
{
var data = new TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>>
TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>> data = new TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>>
{
{ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 } },
{ new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } }
{ new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } },
};
return data;
}
/// <summary>
/// Tests that CopyTo correctly copies elements.
/// </summary>
/// <param name="source">The source list.</param>
/// <param name="destination">The destination list.</param>
/// <param name="index">The starting index.</param>
/// <param name="expected">The expected result.</param>
[Theory]
[MemberData(nameof(CopyTo_Valid_Correct_TestData))]
public static void CopyTo_Valid_Correct(IReadOnlyList<int> source, IList<int> destination, int index, IList<int> expected)
@@ -29,20 +43,30 @@ namespace Jellyfin.Extensions.Tests
Assert.Equal(expected, destination);
}
/// <summary>
/// Gets test data for invalid CopyTo operations that should throw.
/// </summary>
/// <returns>The test data.</returns>
public static TheoryData<IReadOnlyList<int>, IList<int>, int> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData()
{
var data = new TheoryData<IReadOnlyList<int>, IList<int>, int>
TheoryData<IReadOnlyList<int>, IList<int>, int> data = new TheoryData<IReadOnlyList<int>, IList<int>, int>
{
{ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 },
{ new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 },
{ new[] { 0, 1, 2 }, Array.Empty<int>(), 0 },
{ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 },
{ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 }
{ new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 },
};
return data;
}
/// <summary>
/// Tests that CopyTo throws ArgumentOutOfRangeException for invalid inputs.
/// </summary>
/// <param name="source">The source list.</param>
/// <param name="destination">The destination list.</param>
/// <param name="index">The starting index.</param>
[Theory]
[MemberData(nameof(CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData))]
public static void CopyTo_Invalid_ThrowsArgumentOutOfRangeException(IReadOnlyList<int> source, IList<int> destination, int index)
@@ -2,6 +2,8 @@
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;CS1570;CS8600;SA1600;SA1309;SA1200</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -11,16 +11,24 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
using Jellyfin.Extensions.Json.Converters;
using Xunit;
/// <summary>
/// Tests for the JsonBoolNumberConverter.
/// </summary>
public class JsonBoolNumberTests
{
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
private readonly JsonSerializerOptions _jsonOptions = new()
{
Converters =
{
new JsonBoolNumberConverter()
}
new JsonBoolNumberConverter(),
},
};
/// <summary>
/// Tests deserialization of numbers to boolean.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="output">The expected output.</param>
[Theory]
[InlineData("1", true)]
[InlineData("0", false)]
@@ -29,21 +37,33 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
[InlineData("false", false)]
public void Deserialize_Number_Valid_Success(string input, bool? output)
{
var value = JsonSerializer.Deserialize<bool>(input, _jsonOptions);
bool value = JsonSerializer.Deserialize<bool>(input, this._jsonOptions);
Assert.Equal(value, output);
}
/// <summary>
/// Tests serialization of boolean values.
/// </summary>
/// <param name="input">The input boolean.</param>
/// <param name="output">The expected output.</param>
[Theory]
[InlineData(true, "true")]
[InlineData(false, "false")]
public void Serialize_Bool_Success(bool input, string output)
{
var value = JsonSerializer.Serialize(input, _jsonOptions);
string value = JsonSerializer.Serialize(input, this._jsonOptions);
Assert.Equal(value, output);
}
/// <summary>
/// Property test that non-zero integers deserialize to true.
/// </summary>
/// <param name="input">The input non-zero integer.</param>
/// <returns>A property test result.</returns>
[Property]
public Property Deserialize_NonZeroInt_True(NonZeroInt input)
=> JsonSerializer.Deserialize<bool>(input.ToString(), _jsonOptions).ToProperty();
{
return JsonSerializer.Deserialize<bool>(input.ToString(), this._jsonOptions).ToProperty();
}
}
}
@@ -14,8 +14,8 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
{
Converters =
{
new JsonBoolStringConverter()
}
new JsonBoolStringConverter(),
},
};
[Theory]
@@ -23,7 +23,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
[InlineData(@"{ ""Value"": ""false"" }", false)]
public void Deserialize_String_Valid_Success(string input, bool output)
{
var s = JsonSerializer.Deserialize<TestStruct>(input, _jsonOptions);
TestStruct s = JsonSerializer.Deserialize<TestStruct>(input, this._jsonOptions);
Assert.Equal(s.Value, output);
}
@@ -32,7 +32,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
[InlineData(false, "false")]
public void Serialize_Bool_Success(bool input, string output)
{
var value = JsonSerializer.Serialize(input, _jsonOptions);
string value = JsonSerializer.Serialize(input, this._jsonOptions);
Assert.Equal(value, output);
}
@@ -7,205 +7,255 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Jellyfin.Extensions.Tests.Json.Models;
using MediaBrowser.Model.Session;
using Xunit;
/// <summary>
/// Tests for JSON comma delimited collection converter.
/// </summary>
public class JsonCommaDelimitedCollectionTests
{
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
private readonly JsonSerializerOptions jsonOptions = new()
{
Converters =
{
new JsonStringEnumConverter()
}
new JsonStringEnumConverter(),
},
};
/// <summary>
/// Tests that deserializing null string value succeeds.
/// </summary>
[Fact]
public void Deserialize_String_Null_Success()
{
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": null }", _jsonOptions);
GenericBodyArrayModel<string>? value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": null }", this.jsonOptions);
Assert.Null(value?.Value);
}
/// <summary>
/// Tests that deserializing empty string succeeds.
/// </summary>
[Fact]
public void Deserialize_Empty_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
GenericBodyArrayModel<string> desiredValue = new()
{
Value = Array.Empty<string>()
Value = [],
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": """" }", _jsonOptions);
GenericBodyArrayModel<string>? value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": """" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing empty string to list throws exception.
/// </summary>
[Fact]
public void Deserialize_EmptyList_Success()
{
var desiredValue = new GenericBodyListModel<string>
GenericBodyListModel<string> desiredValue = new()
{
Value = []
Value = [],
};
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<GenericBodyListModel<string>>(@"{ ""Value"": """" }", _jsonOptions));
_ = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<GenericBodyListModel<string>>(@"{ ""Value"": """" }", this.jsonOptions));
}
/// <summary>
/// Tests that deserializing empty string to IReadOnlyList succeeds.
/// </summary>
[Fact]
public void Deserialize_EmptyIReadOnlyList_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<string>
GenericBodyIReadOnlyListModel<string> desiredValue = new()
{
Value = []
Value = [],
};
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": """" }", _jsonOptions);
GenericBodyIReadOnlyListModel<string>? value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": """" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing comma-delimited string succeeds.
/// </summary>
[Fact]
public void Deserialize_String_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
GenericBodyArrayModel<string> desiredValue = new()
{
Value = ["a", "b", "c"]
Value = ["a", "b", "c"],
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions);
GenericBodyArrayModel<string>? value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a,b,c"" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing comma-delimited string to list throws exception.
/// </summary>
[Fact]
public void Deserialize_StringList_Valid_Success()
{
var desiredValue = new GenericBodyListModel<string>
GenericBodyListModel<string> desiredValue = new()
{
Value = ["a", "b", "c"]
Value = ["a", "b", "c"],
};
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<GenericBodyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions));
_ = Assert.Throws<InvalidOperationException>(() => JsonSerializer.Deserialize<GenericBodyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", this.jsonOptions));
}
/// <summary>
/// Tests that deserializing comma-delimited string with spaces succeeds.
/// </summary>
[Fact]
public void Deserialize_String_Space_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
GenericBodyArrayModel<string> desiredValue = new()
{
Value = ["a", "b", "c"]
Value = ["a", "b", "c"],
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions);
GenericBodyArrayModel<string>? value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a, b, c"" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing comma-delimited enum string succeeds.
/// </summary>
[Fact]
public void Deserialize_GenericCommandType_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
GenericBodyArrayModel<GeneralCommandType> desiredValue = new()
{
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown],
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions);
GenericBodyArrayModel<GeneralCommandType>? value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing comma-delimited enum string with empty entry succeeds.
/// </summary>
[Fact]
public void Deserialize_GenericCommandType_EmptyEntry_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
GenericBodyArrayModel<GeneralCommandType> desiredValue = new()
{
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown],
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", _jsonOptions);
GenericBodyArrayModel<GeneralCommandType>? value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,,MoveDown"" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing comma-delimited enum string with invalid value succeeds.
/// </summary>
[Fact]
public void Deserialize_GenericCommandType_Invalid_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
GenericBodyArrayModel<GeneralCommandType> desiredValue = new()
{
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown],
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAValidCommand,MoveDown"" }", _jsonOptions);
GenericBodyArrayModel<GeneralCommandType>? value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,TotallyNotAValidCommand,MoveDown"" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing comma-delimited enum string with spaces succeeds.
/// </summary>
[Fact]
public void Deserialize_GenericCommandType_Space_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
GenericBodyArrayModel<GeneralCommandType> desiredValue = new()
{
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown],
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions);
GenericBodyArrayModel<GeneralCommandType>? value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing JSON array of strings succeeds.
/// </summary>
[Fact]
public void Deserialize_String_Array_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<string>
GenericBodyArrayModel<string> desiredValue = new()
{
Value = ["a", "b", "c"]
Value = ["a", "b", "c"],
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions);
GenericBodyArrayModel<string>? value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing JSON array of enums succeeds.
/// </summary>
[Fact]
public void Deserialize_GenericCommandType_Array_Valid_Success()
{
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
GenericBodyArrayModel<GeneralCommandType> desiredValue = new()
{
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown],
};
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions);
GenericBodyArrayModel<GeneralCommandType>? value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that serializing readonly collection succeeds.
/// </summary>
[Fact]
public void Serialize_GenericCommandType_ReadOnlyArray_Valid_Success()
{
var valueToSerialize = new GenericBodyIReadOnlyCollectionModel<GeneralCommandType>
GenericBodyIReadOnlyCollectionModel<GeneralCommandType> valueToSerialize = new()
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }.AsReadOnly()
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }.AsReadOnly(),
};
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyCollectionModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyCollectionModel<GeneralCommandType>>(valueToSerialize, this.jsonOptions);
Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
}
/// <summary>
/// Tests that serializing immutable array succeeds.
/// </summary>
[Fact]
public void Serialize_GenericCommandType_ImmutableArrayArray_Valid_Success()
{
var valueToSerialize = new GenericBodyIReadOnlyCollectionModel<GeneralCommandType>
GenericBodyIReadOnlyCollectionModel<GeneralCommandType> valueToSerialize = new()
{
Value = ImmutableArray.Create(new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown })
Value = ImmutableArray.Create([GeneralCommandType.MoveUp, GeneralCommandType.MoveDown]),
};
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyCollectionModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyCollectionModel<GeneralCommandType>>(valueToSerialize, this.jsonOptions);
Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
}
/// <summary>
/// Tests that serializing list succeeds.
/// </summary>
[Fact]
public void Serialize_GenericCommandType_List_Valid_Success()
{
var valueToSerialize = new GenericBodyIReadOnlyListModel<GeneralCommandType>
GenericBodyIReadOnlyListModel<GeneralCommandType> valueToSerialize = new()
{
Value = new List<GeneralCommandType> { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown],
};
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(valueToSerialize, this.jsonOptions);
Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
}
}
@@ -4,104 +4,127 @@
namespace Jellyfin.Extensions.Tests.Json.Converters
{
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Jellyfin.Extensions.Tests.Json.Models;
using MediaBrowser.Model.Session;
using Xunit;
/// <summary>
/// Tests for JSON comma delimited IReadOnlyList converter.
/// </summary>
public class JsonCommaDelimitedIReadOnlyListTests
{
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
private readonly JsonSerializerOptions jsonOptions = new()
{
Converters =
{
new JsonStringEnumConverter()
}
new JsonStringEnumConverter(),
},
};
/// <summary>
/// Tests that deserializing comma-delimited string succeeds.
/// </summary>
[Fact]
public void Deserialize_String_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<string>
GenericBodyIReadOnlyListModel<string> desiredValue = new()
{
Value = new[] { "a", "b", "c" }
Value = ["a", "b", "c"],
};
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", _jsonOptions);
GenericBodyIReadOnlyListModel<string>? value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing comma-delimited string with spaces succeeds.
/// </summary>
[Fact]
public void Deserialize_String_Space_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<string>
GenericBodyIReadOnlyListModel<string> desiredValue = new()
{
Value = new[] { "a", "b", "c" }
Value = ["a", "b", "c"],
};
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a, b, c"" }", _jsonOptions);
GenericBodyIReadOnlyListModel<string>? value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a, b, c"" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing comma-delimited enum string succeeds.
/// </summary>
[Fact]
public void Deserialize_GenericCommandType_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
GenericBodyIReadOnlyListModel<GeneralCommandType> desiredValue = new()
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown],
};
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", _jsonOptions);
GenericBodyIReadOnlyListModel<GeneralCommandType>? value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing comma-delimited enum string with spaces succeeds.
/// </summary>
[Fact]
public void Deserialize_GenericCommandType_Space_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
GenericBodyIReadOnlyListModel<GeneralCommandType> desiredValue = new()
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown],
};
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", _jsonOptions);
GenericBodyIReadOnlyListModel<GeneralCommandType>? value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing JSON array of strings succeeds.
/// </summary>
[Fact]
public void Deserialize_String_Array_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<string>
GenericBodyIReadOnlyListModel<string> desiredValue = new()
{
Value = new[] { "a", "b", "c" }
Value = ["a", "b", "c"],
};
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", _jsonOptions);
GenericBodyIReadOnlyListModel<string>? value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that deserializing JSON array of enums succeeds.
/// </summary>
[Fact]
public void Deserialize_GenericCommandType_Array_Valid_Success()
{
var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
GenericBodyIReadOnlyListModel<GeneralCommandType> desiredValue = new()
{
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown],
};
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", _jsonOptions);
GenericBodyIReadOnlyListModel<GeneralCommandType>? value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", this.jsonOptions);
Assert.Equal(desiredValue.Value, value?.Value);
}
/// <summary>
/// Tests that serializing IReadOnlyList succeeds.
/// </summary>
[Fact]
public void Serialize_GenericCommandType_IReadOnlyList_Valid_Success()
{
var valueToSerialize = new GenericBodyIReadOnlyListModel<GeneralCommandType>
GenericBodyIReadOnlyListModel<GeneralCommandType> valueToSerialize = new()
{
Value = new List<GeneralCommandType> { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
Value = [GeneralCommandType.MoveUp, GeneralCommandType.MoveDown],
};
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(valueToSerialize, _jsonOptions);
string value = JsonSerializer.Serialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(valueToSerialize, this.jsonOptions);
Assert.Equal(@"{""Value"":[""MoveUp"",""MoveDown""]}", value);
}
}
@@ -2,115 +2,116 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Extensions.Tests.Json.Converters;
using System.Text.Json;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
using Xunit;
public class JsonDefaultStringEnumConverterTests
namespace Jellyfin.Extensions.Tests.Json.Converters
{
private readonly JsonSerializerOptions _jsonOptions = new() { Converters = { new JsonDefaultStringEnumConverterFactory() } };
using System.Text.Json;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
using Xunit;
/// <summary>
/// Test to ensure that `null` and empty string are deserialized to the default value.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="output">The expected enum value.</param>
[Theory]
[InlineData("\"\"", MediaStreamProtocol.http)]
[InlineData("\"Http\"", MediaStreamProtocol.http)]
[InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum_Direct(string input, MediaStreamProtocol output)
public class JsonDefaultStringEnumConverterTests
{
var value = JsonSerializer.Deserialize<MediaStreamProtocol>(input, _jsonOptions);
Assert.Equal(output, value);
}
private readonly JsonSerializerOptions _jsonOptions = new() { Converters = { new JsonDefaultStringEnumConverterFactory() } };
/// <summary>
/// Test to ensure that `null` and empty string are deserialized to the default value.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="output">The expected enum value.</param>
[Theory]
[InlineData(null, MediaStreamProtocol.http)]
[InlineData("\"\"", MediaStreamProtocol.http)]
[InlineData("\"Http\"", MediaStreamProtocol.http)]
[InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum(string? input, MediaStreamProtocol output)
{
input ??= "null";
var json = $"{{ \"EnumValue\": {input} }}";
var value = JsonSerializer.Deserialize<TestClass>(json, _jsonOptions);
Assert.NotNull(value);
Assert.Equal(output, value.EnumValue);
}
/// <summary>
/// Test to ensure that `null` and empty string are deserialized to the default value.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="output">The expected enum value.</param>
[Theory]
[InlineData("\"\"", MediaStreamProtocol.http)]
[InlineData("\"Http\"", MediaStreamProtocol.http)]
[InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum_Direct(string input, MediaStreamProtocol output)
{
MediaStreamProtocol value = JsonSerializer.Deserialize<MediaStreamProtocol>(input, this._jsonOptions);
Assert.Equal(output, value);
}
/// <summary>
/// Test to ensure that empty string is deserialized to the default value,
/// and `null` is deserialized to `null`.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="output">The expected enum value.</param>
[Theory]
[InlineData(null, null)]
[InlineData("\"\"", MediaStreamProtocol.http)]
[InlineData("\"Http\"", MediaStreamProtocol.http)]
[InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum_Nullable(string? input, MediaStreamProtocol? output)
{
input ??= "null";
var json = $"{{ \"EnumValue\": {input} }}";
var value = JsonSerializer.Deserialize<NullTestClass>(json, _jsonOptions);
Assert.NotNull(value);
Assert.Equal(output, value.EnumValue);
}
/// <summary>
/// Test to ensure that `null` and empty string are deserialized to the default value.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="output">The expected enum value.</param>
[Theory]
[InlineData(null, MediaStreamProtocol.http)]
[InlineData("\"\"", MediaStreamProtocol.http)]
[InlineData("\"Http\"", MediaStreamProtocol.http)]
[InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum(string? input, MediaStreamProtocol output)
{
input ??= "null";
string json = $"{{ \"EnumValue\": {input} }}";
TestClass value = JsonSerializer.Deserialize<TestClass>(json, this._jsonOptions);
Assert.NotNull(value);
Assert.Equal(output, value.EnumValue);
}
/// <summary>
/// Ensures that the roundtrip serialization & deserialization is successful.
/// </summary>
/// <param name="input">Input enum.</param>
/// <param name="output">Output enum.</param>
[Theory]
[InlineData(MediaStreamProtocol.http, MediaStreamProtocol.http)]
[InlineData(MediaStreamProtocol.hls, MediaStreamProtocol.hls)]
public void Enum_RoundTrip(MediaStreamProtocol input, MediaStreamProtocol output)
{
var inputObj = new TestClass { EnumValue = input };
/// <summary>
/// Test to ensure that empty string is deserialized to the default value,
/// and `null` is deserialized to `null`.
/// </summary>
/// <param name="input">The input string.</param>
/// <param name="output">The expected enum value.</param>
[Theory]
[InlineData(null, null)]
[InlineData("\"\"", MediaStreamProtocol.http)]
[InlineData("\"Http\"", MediaStreamProtocol.http)]
[InlineData("\"Hls\"", MediaStreamProtocol.hls)]
public void Deserialize_Enum_Nullable(string? input, MediaStreamProtocol? output)
{
input ??= "null";
string json = $"{{ \"EnumValue\": {input} }}";
NullTestClass value = JsonSerializer.Deserialize<NullTestClass>(json, this._jsonOptions);
Assert.NotNull(value);
Assert.Equal(output, value.EnumValue);
}
var outputObj = JsonSerializer.Deserialize<TestClass>(JsonSerializer.Serialize(inputObj, _jsonOptions), _jsonOptions);
/// <summary>
/// Ensures that the roundtrip serialization &amp; deserialization is successful.
/// </summary>
/// <param name="input">Input enum.</param>
/// <param name="output">Output enum.</param>
[Theory]
[InlineData(MediaStreamProtocol.http, MediaStreamProtocol.http)]
[InlineData(MediaStreamProtocol.hls, MediaStreamProtocol.hls)]
public void Enum_RoundTrip(MediaStreamProtocol input, MediaStreamProtocol output)
{
TestClass inputObj = new() { EnumValue = input };
Assert.NotNull(outputObj);
Assert.Equal(output, outputObj.EnumValue);
}
TestClass? outputObj = JsonSerializer.Deserialize<TestClass>(JsonSerializer.Serialize(inputObj, this._jsonOptions), this._jsonOptions);
/// <summary>
/// Ensures that the roundtrip serialization & deserialization is successful, including null.
/// </summary>
/// <param name="input">Input enum.</param>
/// <param name="output">Output enum.</param>
[Theory]
[InlineData(MediaStreamProtocol.http, MediaStreamProtocol.http)]
[InlineData(MediaStreamProtocol.hls, MediaStreamProtocol.hls)]
[InlineData(null, null)]
public void Enum_RoundTrip_Nullable(MediaStreamProtocol? input, MediaStreamProtocol? output)
{
var inputObj = new NullTestClass { EnumValue = input };
Assert.NotNull(outputObj);
Assert.Equal(output, outputObj.EnumValue);
}
var outputObj = JsonSerializer.Deserialize<NullTestClass>(JsonSerializer.Serialize(inputObj, _jsonOptions), _jsonOptions);
/// <summary>
/// Ensures that the roundtrip serialization &amp; deserialization is successful, including null.
/// </summary>
/// <param name="input">Input enum.</param>
/// <param name="output">Output enum.</param>
[Theory]
[InlineData(MediaStreamProtocol.http, MediaStreamProtocol.http)]
[InlineData(MediaStreamProtocol.hls, MediaStreamProtocol.hls)]
[InlineData(null, null)]
public void Enum_RoundTrip_Nullable(MediaStreamProtocol? input, MediaStreamProtocol? output)
{
NullTestClass inputObj = new() { EnumValue = input };
Assert.NotNull(outputObj);
Assert.Equal(output, outputObj.EnumValue);
}
NullTestClass? outputObj = JsonSerializer.Deserialize<NullTestClass>(JsonSerializer.Serialize(inputObj, this._jsonOptions), this._jsonOptions);
private sealed class TestClass
{
public MediaStreamProtocol EnumValue { get; set; }
}
Assert.NotNull(outputObj);
Assert.Equal(output, outputObj.EnumValue);
}
private sealed class NullTestClass
{
public MediaStreamProtocol? EnumValue { get; set; }
private sealed class TestClass
{
public MediaStreamProtocol EnumValue { get; set; }
}
private sealed class NullTestClass
{
public MediaStreamProtocol? EnumValue { get; set; }
}
}
}
@@ -2,31 +2,32 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Extensions.Tests.Json.Converters;
using System.Text.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Session;
using Xunit;
public class JsonFlagEnumTests
namespace Jellyfin.Extensions.Tests.Json.Converters
{
private readonly JsonSerializerOptions _jsonOptions = new()
using System.Text.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Session;
using Xunit;
public class JsonFlagEnumTests
{
Converters =
private readonly JsonSerializerOptions _jsonOptions = new()
{
new JsonFlagEnumConverter<TranscodeReason>()
Converters =
{
new JsonFlagEnumConverter<TranscodeReason>(),
},
};
[Theory]
[InlineData(TranscodeReason.AudioIsExternal | TranscodeReason.ContainerNotSupported, "[\"ContainerNotSupported\",\"AudioIsExternal\"]")]
[InlineData(TranscodeReason.AudioIsExternal | TranscodeReason.ContainerNotSupported | TranscodeReason.VideoBitDepthNotSupported, "[\"ContainerNotSupported\",\"AudioIsExternal\",\"VideoBitDepthNotSupported\"]")]
[InlineData((TranscodeReason)0, "[]")]
public void Serialize_Transcode_Reason(TranscodeReason transcodeReason, string output)
{
string result = JsonSerializer.Serialize(transcodeReason, this._jsonOptions);
Assert.Equal(output, result);
}
};
[Theory]
[InlineData(TranscodeReason.AudioIsExternal | TranscodeReason.ContainerNotSupported, "[\"ContainerNotSupported\",\"AudioIsExternal\"]")]
[InlineData(TranscodeReason.AudioIsExternal | TranscodeReason.ContainerNotSupported | TranscodeReason.VideoBitDepthNotSupported, "[\"ContainerNotSupported\",\"AudioIsExternal\",\"VideoBitDepthNotSupported\"]")]
[InlineData((TranscodeReason)0, "[]")]
public void Serialize_Transcode_Reason(TranscodeReason transcodeReason, string output)
{
var result = JsonSerializer.Serialize(transcodeReason, _jsonOptions);
Assert.Equal(output, result);
}
}
@@ -15,49 +15,49 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
public JsonGuidConverterTests()
{
_options = new JsonSerializerOptions();
_options.Converters.Add(new JsonGuidConverter());
this._options = new JsonSerializerOptions();
this._options.Converters.Add(new JsonGuidConverter());
}
[Fact]
public void Deserialize_Valid_Success()
{
Guid value = JsonSerializer.Deserialize<Guid>(@"""a852a27afe324084ae66db579ee3ee18""", _options);
Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value);
Guid value = JsonSerializer.Deserialize<Guid>(@"""a852a27afe324084ae66db579ee3ee18""", this._options);
Assert.Equal(new("a852a27afe324084ae66db579ee3ee18"), value);
}
[Fact]
public void Deserialize_ValidDashed_Success()
{
Guid value = JsonSerializer.Deserialize<Guid>(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", _options);
Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value);
Guid value = JsonSerializer.Deserialize<Guid>(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", this._options);
Assert.Equal(new("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value);
}
[Fact]
public void Roundtrip_Valid_Success()
{
Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18");
string value = JsonSerializer.Serialize(guid, _options);
Assert.Equal(guid, JsonSerializer.Deserialize<Guid>(value, _options));
Guid guid = new("a852a27afe324084ae66db579ee3ee18");
string value = JsonSerializer.Serialize(guid, this._options);
Assert.Equal(guid, JsonSerializer.Deserialize<Guid>(value, this._options));
}
[Fact]
public void Deserialize_Null_EmptyGuid()
{
Assert.Equal(Guid.Empty, JsonSerializer.Deserialize<Guid>("null", _options));
Assert.Equal(Guid.Empty, JsonSerializer.Deserialize<Guid>("null", this._options));
}
[Fact]
public void Serialize_EmptyGuid_EmptyGuid()
{
Assert.Equal($"\"{Guid.Empty:N}\"", JsonSerializer.Serialize(Guid.Empty, _options));
Assert.Equal($"\"{Guid.Empty:N}\"", JsonSerializer.Serialize(Guid.Empty, this._options));
}
[Fact]
public void Serialize_Valid_NoDash_Success()
{
var guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
var str = JsonSerializer.Serialize(guid, _options);
Guid guid = new("531797E9-9457-40E0-88BC-B1D6D38752FA");
string str = JsonSerializer.Serialize(guid, this._options);
Assert.Equal($"\"{guid:N}\"", str);
}
@@ -65,7 +65,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
public void Serialize_Nullable_Success()
{
Guid? guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
var str = JsonSerializer.Serialize(guid, _options);
string str = JsonSerializer.Serialize(guid, this._options);
Assert.Equal($"\"{guid:N}\"", str);
}
}
@@ -15,69 +15,69 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
public JsonNullableGuidConverterTests()
{
_options = new JsonSerializerOptions();
_options.Converters.Add(new JsonNullableGuidConverter());
this._options = new JsonSerializerOptions();
this._options.Converters.Add(new JsonNullableGuidConverter());
}
[Fact]
public void Deserialize_Valid_Success()
{
Guid? value = JsonSerializer.Deserialize<Guid?>(@"""a852a27afe324084ae66db579ee3ee18""", _options);
Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value);
Guid? value = JsonSerializer.Deserialize<Guid?>(@"""a852a27afe324084ae66db579ee3ee18""", this._options);
Assert.Equal(new("a852a27afe324084ae66db579ee3ee18"), value);
}
[Fact]
public void Deserialize_ValidDashed_Success()
{
Guid? value = JsonSerializer.Deserialize<Guid?>(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", _options);
Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value);
Guid? value = JsonSerializer.Deserialize<Guid?>(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", this._options);
Assert.Equal(new("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value);
}
[Fact]
public void Roundtrip_Valid_Success()
{
Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18");
string value = JsonSerializer.Serialize(guid, _options);
Assert.Equal(guid, JsonSerializer.Deserialize<Guid?>(value, _options));
Guid guid = new("a852a27afe324084ae66db579ee3ee18");
string value = JsonSerializer.Serialize(guid, this._options);
Assert.Equal(guid, JsonSerializer.Deserialize<Guid?>(value, this._options));
}
[Fact]
public void Deserialize_Null_Null()
{
Assert.Null(JsonSerializer.Deserialize<Guid?>("null", _options));
Assert.Null(JsonSerializer.Deserialize<Guid?>("null", this._options));
}
[Fact]
public void Deserialize_EmptyGuid_EmptyGuid()
{
Assert.Equal(Guid.Empty, JsonSerializer.Deserialize<Guid?>(@"""00000000-0000-0000-0000-000000000000""", _options));
Assert.Equal(Guid.Empty, JsonSerializer.Deserialize<Guid?>(@"""00000000-0000-0000-0000-000000000000""", this._options));
}
[Fact]
public void Serialize_EmptyGuid_Null()
{
Assert.Equal("null", JsonSerializer.Serialize((Guid?)Guid.Empty, _options));
Assert.Equal("null", JsonSerializer.Serialize((Guid?)Guid.Empty, this._options));
}
[Fact]
public void Serialize_Null_Null()
{
Assert.Equal("null", JsonSerializer.Serialize((Guid?)null, _options));
Assert.Equal("null", JsonSerializer.Serialize((Guid?)null, this._options));
}
[Fact]
public void Serialize_Valid_NoDash_Success()
{
var guid = (Guid?)new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
var str = JsonSerializer.Serialize(guid, _options);
Guid? guid = new("531797E9-9457-40E0-88BC-B1D6D38752FA");
string str = JsonSerializer.Serialize(guid, this._options);
Assert.Equal($"\"{guid:N}\"", str);
}
[Fact]
public void Serialize_Nullable_Success()
{
Guid? guid = new Guid("531797E9-9457-40E0-88BC-B1D6D38752FA");
var str = JsonSerializer.Serialize(guid, _options);
Guid? guid = new("531797E9-9457-40E0-88BC-B1D6D38752FA");
string str = JsonSerializer.Serialize(guid, this._options);
Assert.Equal($"\"{guid:N}\"", str);
}
}
@@ -14,8 +14,8 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
{
Converters =
{
new JsonStringConverter()
}
new JsonStringConverter(),
},
};
[Theory]
@@ -26,7 +26,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
[InlineData("false", "false")]
public void Deserialize_String_Valid_Success(string input, string output)
{
var deserialized = JsonSerializer.Deserialize<string>(input, _jsonSerializerOptions);
string? deserialized = JsonSerializer.Deserialize<string>(input, this._jsonSerializerOptions);
Assert.Equal(deserialized, output);
}
@@ -35,7 +35,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
{
const string? input = "123";
const int output = 123;
var deserialized = JsonSerializer.Deserialize<int>(input, _jsonSerializerOptions);
int deserialized = JsonSerializer.Deserialize<int>(input, this._jsonSerializerOptions);
Assert.Equal(output, deserialized);
}
}
@@ -15,25 +15,25 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
public JsonVersionConverterTests()
{
_options = new JsonSerializerOptions();
_options.Converters.Add(new JsonVersionConverter());
this._options = new JsonSerializerOptions();
this._options.Converters.Add(new JsonVersionConverter());
}
[Fact]
public void Deserialize_Version_Success()
{
var input = "\"1.025.222\"";
var output = new Version(1, 25, 222);
var deserializedInput = JsonSerializer.Deserialize<Version>(input, _options);
string input = "\"1.025.222\"";
Version output = new(1, 25, 222);
Version? deserializedInput = JsonSerializer.Deserialize<Version>(input, this._options);
Assert.Equal(output, deserializedInput);
}
[Fact]
public void Serialize_Version_Success()
{
var input = new Version(1, 09, 59);
var output = "\"1.9.59\"";
var serializedInput = JsonSerializer.Serialize(input, _options);
Version input = new(1, 09, 59);
string output = "\"1.9.59\"";
string serializedInput = JsonSerializer.Serialize(input, this._options);
Assert.Equal(output, serializedInput);
}
}
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "en/g4Rojfa8=",
"dgSpecHash": "cBK/w0WVJEs=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\tests\\Jellyfin.Extensions.Tests\\Jellyfin.Extensions.Tests.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "YQ4opaxxEO8=",
"dgSpecHash": "nbns8k6jJuA=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\tests\\Jellyfin.LiveTv.Tests\\Jellyfin.LiveTv.Tests.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "/lT1/xD3XnU=",
"dgSpecHash": "hvFFds1OJpc=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\tests\\Jellyfin.MediaEncoding.Hls.Tests\\Jellyfin.MediaEncoding.Hls.Tests.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "0nvUEzLEN94=",
"dgSpecHash": "5g3aj0khPqM=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\tests\\Jellyfin.MediaEncoding.Keyframes.Tests\\Jellyfin.MediaEncoding.Keyframes.Tests.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "SBmVy71j/UM=",
"dgSpecHash": "2rGlge7UbGY=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\tests\\Jellyfin.MediaEncoding.Tests\\Jellyfin.MediaEncoding.Tests.csproj",
"expectedPackageFiles": [
@@ -53,8 +53,7 @@ namespace Jellyfin.Model.Tests.Cryptography
new Dictionary<string, string>()
{
{ "iterations", "1000" },
{ "m", "120" }
}));
{ "m", "120" },`r`n }));
// Id + hash
data.Add(
@@ -83,7 +82,8 @@ namespace Jellyfin.Model.Tests.Cryptography
Array.Empty<byte>(),
new Dictionary<string, string>()
{
{ "iterations", "1000" }
{ "iterations", "1000" },
{ "iterations", "1000" },
}));
// Id + parameters + hash
data.Add(
@@ -95,7 +95,7 @@ namespace Jellyfin.Model.Tests.Cryptography
new Dictionary<string, string>()
{
{ "iterations", "1000" },
{ "m", "120" }
{ "m", "120" },
}));
// Id + parameters + salt + hash
data.Add(
@@ -107,7 +107,7 @@ namespace Jellyfin.Model.Tests.Cryptography
new Dictionary<string, string>()
{
{ "iterations", "1000" },
{ "m", "120" }
{ "m", "120" },
}));
return data;
}
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "S2M6GE9E6Mw=",
"dgSpecHash": "E+3Xf4aE0A8=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\tests\\Jellyfin.Model.Tests\\Jellyfin.Model.Tests.csproj",
"expectedPackageFiles": [
@@ -29,6 +29,6 @@ namespace Jellyfin.Naming.Tests.AudioBook
var info1 = new AudioBookFileInfo(string.Empty, string.Empty);
var info2 = new AudioBookFileInfo(string.Empty, string.Empty);
Assert.Equal(0, info1.CompareTo(info2));
}
},
}
}
}
@@ -38,7 +38,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
"Ready Player One (2020)/audiobook.mp3",
"Ready Player One (2020)/extra.mp3",
".mp3"
".mp3",
};
var resolver = GetResolver();
@@ -89,7 +89,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
"Superman/extra.mp3",
"Batman/ Chapter 1 .mp3",
"Batman/Chapter 1[loss-less].mp3"
"Batman/Chapter 1[loss-less].mp3",
};
var resolver = GetResolver();
@@ -166,7 +166,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
Name = " ",
Path = " .mp3",
Year = null
}
},
};
var resolver = GetResolver();
@@ -183,7 +183,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
Assert.Equal(data[i].Name, result[i].Name);
Assert.Equal(data[i].Year, result[i].Year);
}
},
}
[Fact]
@@ -192,7 +192,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
var files = new[]
{
"Harry Potter and the Deathly Hallows/Chapter 1.ogg",
"Harry Potter and the Deathly Hallows/Harry Potter and the Deathly Hallows.nfo"
"Harry Potter and the Deathly Hallows/Harry Potter and the Deathly Hallows.nfo",
};
var resolver = GetResolver();
@@ -212,7 +212,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
var files = new[]
{
"Harry Potter and the Deathly Hallows/Chapter 1.mp3",
"Harry Potter and the Deathly Hallows/Harry Potter and the Deathly Hallows trailer.mp3"
"Harry Potter and the Deathly Hallows/Harry Potter and the Deathly Hallows trailer.mp3",
};
var resolver = GetResolver();
@@ -231,7 +231,7 @@ namespace Jellyfin.Naming.Tests.AudioBook
{
var files = new[]
{
"Harry Potter and the Deathly Hallows trailer.mp3"
"Harry Potter and the Deathly Hallows trailer.mp3",
};
var resolver = GetResolver();
@@ -271,6 +271,6 @@ namespace Jellyfin.Naming.Tests.AudioBook
public string Name;
public string Path;
public int? Year;
}
},
}
}
}
@@ -64,6 +64,6 @@ namespace Jellyfin.Naming.Tests.AudioBook
var result = new AudioBookResolver(_namingOptions).Resolve(string.Empty);
Assert.Null(result);
}
},
}
}
}
@@ -32,6 +32,6 @@ namespace Jellyfin.Naming.Tests.Common
exp.Expression = "test";
Assert.Equal("test", exp.Expression);
Assert.NotNull(exp.Regex);
}
},
}
}
}
@@ -122,4 +122,4 @@ public class ExternalPathParserTests
Assert.Equal(isForced, actual.IsForced);
Assert.Equal(isHearingImpaired, actual.IsHearingImpaired);
}
}
}
@@ -49,6 +49,6 @@ namespace Jellyfin.Naming.Tests.Music
var parser = new AlbumParser(_namingOptions);
Assert.Equal(result, parser.IsMultiPart(path));
}
},
}
}
}
@@ -25,6 +25,6 @@ namespace Jellyfin.Naming.Tests.TV
var result = _resolver.Resolve(path, false, null, null, true);
Assert.Equal(episodeNumber, result?.EpisodeNumber);
}
},
}
}
}
@@ -31,6 +31,6 @@ namespace Jellyfin.Naming.Tests.TV
Assert.Equal(month, result?.Month);
Assert.Equal(day, result?.Day);
Assert.Equal(seriesName, result?.SeriesName, true);
}
},
}
}
}
@@ -95,6 +95,6 @@ namespace Jellyfin.Naming.Tests.TV
.Parse(path, false);
Assert.Equal(expected, result.EpisodeNumber);
}
},
}
}
}
@@ -33,6 +33,6 @@ namespace Jellyfin.Naming.Tests.TV
var result = _resolver.Resolve(path, false);
Assert.Equal(episodeNumber, result?.EpisodeNumber);
}
},
}
}
}
@@ -103,6 +103,6 @@ namespace Jellyfin.Naming.Tests.TV
var res = p.Parse("ABC_2019_10_21 11:00:00", false);
Assert.True(res.Success);
}
},
}
}
}
@@ -78,6 +78,6 @@ namespace Jellyfin.Naming.Tests.TV
var result = _episodePathParser.Parse(filename, false);
Assert.Equal(result.EndingEpisodeNumber, endingEpisodeNumber);
}
},
}
}
}
@@ -63,6 +63,6 @@ namespace Jellyfin.Naming.Tests.TV
var result = _resolver.Resolve(path, false);
Assert.Equal(expected, result?.SeasonNumber);
}
},
}
}
}
@@ -87,4 +87,4 @@ public class SeasonPathParserTests
Assert.Equal(seasonNumber, result.SeasonNumber);
Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
}
}
}
@@ -28,6 +28,6 @@ namespace Jellyfin.Naming.Tests.TV
Assert.Equal(name, res.SeriesName);
Assert.True(res.Success);
}
},
}
}
}
@@ -29,6 +29,6 @@ namespace Jellyfin.Naming.Tests.TV
var res = SeriesResolver.Resolve(_namingOptions, path);
Assert.Equal(name, res.Name);
}
},
}
}
}
@@ -48,6 +48,6 @@ namespace Jellyfin.Naming.Tests.TV
Assert.Null(result!.StubType);
Assert.Equal(episodeEndNumber, result!.EndingEpisodeNumber);
Assert.False(result!.IsByDate);
}
},
}
}
}
@@ -32,4 +32,4 @@ public class TvParserHelpersTest
Assert.False(successful);
Assert.Null(parsed);
}
}
}
@@ -65,6 +65,6 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Equal(expectedName, result.Name, true);
Assert.Equal(expectedYear, result.Year);
}
},
}
}
}
@@ -52,6 +52,6 @@ namespace Jellyfin.Naming.Tests.Video
{
Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out var newName));
Assert.True(string.IsNullOrEmpty(newName));
}
},
}
}
}
@@ -166,6 +166,6 @@ namespace Jellyfin.Naming.Tests.Video
var res = ExtraRuleResolver.GetExtraInfo("extra.mp4", options);
Assert.Equal(rule, res.Rule);
}
},
}
}
}
@@ -72,7 +72,7 @@ namespace Jellyfin.Naming.Tests.Video
else
{
Assert.Equal(format3D, result.Format3D, true);
}
}
},
},
}
}
}
@@ -22,7 +22,7 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/X-Men Days of Future Past/X-Men Days of Future Past - 1080p.mkv",
"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
"/movies/X-Men Days of Future Past/X-Men Days of Future Past - [hsbs].mkv",
"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv"
"/movies/X-Men Days of Future Past/X-Men Days of Future Past [hsbs].mkv",
};
var result = VideoListResolver.Resolve(
@@ -41,7 +41,7 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/X-Men Days of Future Past/X-Men Days of Future Past - apple.mkv",
"/movies/X-Men Days of Future Past/X-Men Days of Future Past-trailer.mp4",
"/movies/X-Men Days of Future Past/X-Men Days of Future Past - banana.mkv",
"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4"
"/movies/X-Men Days of Future Past/X-Men Days of Future Past [banana].mp4",
};
var result = VideoListResolver.Resolve(
@@ -59,7 +59,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1925 version.mkv",
"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv"
"/movies/The Phantom of the Opera (1925)/The Phantom of the Opera (1925) - 1929 version.mkv",
};
var result = VideoListResolver.Resolve(
@@ -81,7 +81,7 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/M/Movie 4.mkv",
"/movies/M/Movie 5.mkv",
"/movies/M/Movie 6.mkv",
"/movies/M/Movie 7.mkv"
"/movies/M/Movie 7.mkv",
};
var result = VideoListResolver.Resolve(
@@ -104,7 +104,7 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Movie/Movie-5.mkv",
"/movies/Movie/Movie-6.mkv",
"/movies/Movie/Movie-7.mkv",
"/movies/Movie/Movie-8.mkv"
"/movies/Movie/Movie-8.mkv",
};
var result = VideoListResolver.Resolve(
@@ -128,7 +128,7 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Mo/Movie 6.mkv",
"/movies/Mo/Movie 7.mkv",
"/movies/Mo/Movie 8.mkv",
"/movies/Mo/Movie 9.mkv"
"/movies/Mo/Movie 9.mkv",
};
var result = VideoListResolver.Resolve(
@@ -148,7 +148,7 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Movie/Movie 2.mkv",
"/movies/Movie/Movie 3.mkv",
"/movies/Movie/Movie 4.mkv",
"/movies/Movie/Movie 5.mkv"
"/movies/Movie/Movie 5.mkv",
};
var result = VideoListResolver.Resolve(
@@ -170,7 +170,7 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Iron Man/Iron Man (2008).mkv",
"/movies/Iron Man/Iron Man (2009).mkv",
"/movies/Iron Man/Iron Man (2010).mkv",
"/movies/Iron Man/Iron Man (2011).mkv"
"/movies/Iron Man/Iron Man (2011).mkv",
};
var result = VideoListResolver.Resolve(
@@ -192,7 +192,7 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Iron Man/Iron Man-bluray.mkv",
"/movies/Iron Man/Iron Man-3d.mkv",
"/movies/Iron Man/Iron Man-3d-hsbs.mkv",
"/movies/Iron Man/Iron Man[test].mkv"
"/movies/Iron Man/Iron Man[test].mkv",
};
var result = VideoListResolver.Resolve(
@@ -221,7 +221,7 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Iron Man/Iron Man - bluray.mkv",
"/movies/Iron Man/Iron Man - 3d.mkv",
"/movies/Iron Man/Iron Man - 3d-hsbs.mkv",
"/movies/Iron Man/Iron Man [test].mkv"
"/movies/Iron Man/Iron Man [test].mkv",
};
var result = VideoListResolver.Resolve(
@@ -245,7 +245,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"/movies/Iron Man/Iron Man - B (2006).mkv",
"/movies/Iron Man/Iron Man - C (2007).mkv"
"/movies/Iron Man/Iron Man - C (2007).mkv",
};
var result = VideoListResolver.Resolve(
@@ -266,7 +266,7 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Iron Man/Iron Man_bluray.mkv",
"/movies/Iron Man/Iron Man_3d.mkv",
"/movies/Iron Man/Iron Man_3d-hsbs.mkv",
"/movies/Iron Man/Iron Man_3d.hsbs.mkv"
"/movies/Iron Man/Iron Man_3d.hsbs.mkv",
};
var result = VideoListResolver.Resolve(
@@ -288,7 +288,7 @@ namespace Jellyfin.Naming.Tests.Video
"/movies/Iron Man/Iron Man (2008).mkv",
"/movies/Iron Man/Iron Man (2009).mkv",
"/movies/Iron Man/Iron Man (2010).mkv",
"/movies/Iron Man/Iron Man (2011).mkv"
"/movies/Iron Man/Iron Man (2011).mkv",
};
var result = VideoListResolver.Resolve(
@@ -305,7 +305,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"/movies/Blade Runner (1982)/Blade Runner (1982) [Final Cut] [1080p HEVC AAC].mkv",
"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv"
"/movies/Blade Runner (1982)/Blade Runner (1982) [EE by ADM] [480p HEVC AAC,AAC,AAC].mkv",
};
var result = VideoListResolver.Resolve(
@@ -322,7 +322,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [1080p] Blu-ray.x264.DTS.mkv",
"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv"
"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) [2160p] Blu-ray.x265.AAC.mkv",
};
var result = VideoListResolver.Resolve(
@@ -405,7 +405,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 1.mkv",
"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv",
};
var result = VideoListResolver.Resolve(
@@ -422,7 +422,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 1].mkv",
"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv"
"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv",
};
var result = VideoListResolver.Resolve(
@@ -438,6 +438,6 @@ namespace Jellyfin.Naming.Tests.Video
var result = VideoListResolver.Resolve(new List<VideoFileInfo>(), _namingOptions).ToList();
Assert.Empty(result);
}
},
}
}
}
+24 -24
View File
@@ -23,7 +23,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006) part2.mkv",
"Bad Boys (2006) part3.mkv",
"Bad Boys (2006) part4.mkv",
"Bad Boys (2006)-trailer.mkv"
"Bad Boys (2006)-trailer.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -38,7 +38,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"Bad Boys (2006).mkv",
"Bad Boys (2007).mkv"
"Bad Boys (2007).mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -52,7 +52,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"Bad Boys 2006.mkv",
"Bad Boys 2007.mkv"
"Bad Boys 2007.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -66,7 +66,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"300 (2006).mkv",
"300 (2007).mkv"
"300 (2007).mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -80,7 +80,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"300 2006.mkv",
"300 2007.mkv"
"300 2007.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -94,7 +94,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"Star Trek 1 - The motion picture.mkv",
"Star Trek 2- The wrath of khan.mkv"
"Star Trek 2- The wrath of khan.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -108,7 +108,7 @@ namespace Jellyfin.Naming.Tests.Video
{
"Red Riding in the Year of Our Lord 1983 (2009).mkv",
"Red Riding in the Year of Our Lord 1980 (2009).mkv",
"Red Riding in the Year of Our Lord 1974 (2009).mkv"
"Red Riding in the Year of Our Lord 1974 (2009).mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -122,7 +122,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"d:/movies/300 2006 part1.mkv",
"d:/movies/300 2006 part2.mkv"
"d:/movies/300 2006 part2.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -140,7 +140,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006).part2.stv.unrated.multi.1080p.bluray.x264-rough.mkv",
"Bad Boys (2006).part3.stv.unrated.multi.1080p.bluray.x264-rough.mkv",
"Bad Boys (2006).part4.stv.unrated.multi.1080p.bluray.x264-rough.mkv",
"Bad Boys (2006)-trailer.mkv"
"Bad Boys (2006)-trailer.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -157,7 +157,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006) 1.mkv",
"Bad Boys (2006) 2.mkv",
"Bad Boys (2006) 3.mkv",
"Bad Boys (2006)-trailer.mkv"
"Bad Boys (2006)-trailer.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -174,7 +174,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006) part2.mkv",
"300 (2006) part3.mkv",
"300 (2006) part4.mkv",
"300 (2006)-trailer.mkv"
"300 (2006)-trailer.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -192,7 +192,7 @@ namespace Jellyfin.Naming.Tests.Video
"Bad Boys (2006) part2.mkv",
"Bad Boys (2006) part3.mkv",
"Bad Boys (2006) parta.mkv",
"Bad Boys (2006)-trailer.mkv"
"Bad Boys (2006)-trailer.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -214,7 +214,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006) part1.mkv",
"300 (2006) part2.mkv",
"300 (2006) part3.mkv",
"300 (2006)-trailer.mkv"
"300 (2006)-trailer.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -230,7 +230,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"blah blah - cd 1",
"blah blah - cd 2"
"blah blah - cd 2",
};
var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList();
@@ -247,7 +247,7 @@ namespace Jellyfin.Naming.Tests.Video
"300a.mkv",
"300b.mkv",
"300c.mkv",
"300-trailer.mkv"
"300-trailer.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -266,7 +266,7 @@ namespace Jellyfin.Naming.Tests.Video
"Avatar",
"Avengers part1.mkv",
"Avengers part2.mkv",
"Avengers part3.mkv"
"Avengers part3.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -295,7 +295,7 @@ namespace Jellyfin.Naming.Tests.Video
"300a.mkv",
"300b.mkv",
"300c.mkv",
"300-trailer.mkv"
"300-trailer.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -319,7 +319,7 @@ namespace Jellyfin.Naming.Tests.Video
"300 (2006) parte.mkv",
"300 (2006) partf.mkv",
"300 (2006) partg.mkv",
"300 (2006)-trailer.mkv"
"300 (2006)-trailer.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -338,7 +338,7 @@ namespace Jellyfin.Naming.Tests.Video
new FileSystemMetadata { FullName = "Bad Boys (2006) part2.mkv", IsDirectory = false },
new FileSystemMetadata { FullName = "300 (2006) part2", IsDirectory = true },
new FileSystemMetadata { FullName = "300 (2006) part3", IsDirectory = true },
new FileSystemMetadata { FullName = "300 (2006) part1", IsDirectory = true }
new FileSystemMetadata { FullName = "300 (2006) part1", IsDirectory = true },
};
var result = StackResolver.Resolve(files, _namingOptions).ToList();
@@ -358,7 +358,7 @@ namespace Jellyfin.Naming.Tests.Video
"Harry Potter and the Deathly Hallows 1.mkv",
"Harry Potter and the Deathly Hallows 2.mkv",
"Harry Potter and the Deathly Hallows 3.mkv",
"Harry Potter and the Deathly Hallows 4.mkv"
"Harry Potter and the Deathly Hallows 4.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -373,7 +373,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part1.mkv",
"Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part2.mkv"
"Neverland (2011)[720p][PG][Voted 6.5][Family-Fantasy]part2.mkv",
};
var result = StackResolver.ResolveFiles(files, _namingOptions).ToList();
@@ -389,7 +389,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 01)",
"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)"
"M:/Movies (DVD)/Movies (Musical)/The Sound of Music/The Sound of Music (1965) (Disc 02)",
};
var result = StackResolver.ResolveDirectories(files, _namingOptions).ToList();
@@ -402,6 +402,6 @@ namespace Jellyfin.Naming.Tests.Video
{
Assert.Equal(fileCount, stack.Files.Count);
Assert.Equal(name, stack.Name);
}
},
}
}
}
@@ -51,7 +51,7 @@ namespace Jellyfin.Naming.Tests.Video
else
{
Assert.Null(stubTypeResult);
}
}
},
},
}
}
}
@@ -41,7 +41,7 @@ namespace Jellyfin.Naming.Tests.Video
"trailer.mkv",
// Same as above
"WillyWonka-trailer.mkv"
"WillyWonka-trailer.mkv",
};
var result = VideoListResolver.Resolve(
@@ -75,7 +75,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"300.mkv",
"300.nfo"
"300.nfo",
};
var result = VideoListResolver.Resolve(
@@ -91,7 +91,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"300.mkv",
"300 - trailer.mkv"
"300 - trailer.mkv",
};
var result = VideoListResolver.Resolve(
@@ -109,7 +109,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"X-Men Days of Future Past - 1080p.mkv",
"X-Men Days of Future Past-trailer.mp4"
"X-Men Days of Future Past-trailer.mp4",
};
var result = VideoListResolver.Resolve(
@@ -128,7 +128,7 @@ namespace Jellyfin.Naming.Tests.Video
{
"X-Men Days of Future Past - 1080p.mkv",
"X-Men Days of Future Past-trailer.mp4",
"X-Men Days of Future Past-trailer2.mp4"
"X-Men Days of Future Past-trailer2.mp4",
};
var result = VideoListResolver.Resolve(
@@ -148,7 +148,7 @@ namespace Jellyfin.Naming.Tests.Video
{
"Looper (2012)-trailer.mkv",
"Looper 2012-trailer.mkv",
"Looper.2012.bluray.720p.x264.mkv"
"Looper.2012.bluray.720p.x264.mkv",
};
var result = VideoListResolver.Resolve(
@@ -167,7 +167,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"/movies/Looper (2012)/Looper (2012)-trailer.mkv",
"/movies/Looper (2012)/Looper.bluray.720p.x264.mkv"
"/movies/Looper (2012)/Looper.bluray.720p.x264.mkv",
};
var result = VideoListResolver.Resolve(
@@ -189,7 +189,7 @@ namespace Jellyfin.Naming.Tests.Video
"My video 2.mkv",
"My video 3.mkv",
"My video 4.mkv",
"My video 5.mkv"
"My video 5.mkv",
};
var result = VideoListResolver.Resolve(
@@ -205,7 +205,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 1",
"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2"
"M:/Movies (DVD)/Movies (Musical)/Sound of Music (1965)/Sound of Music Disc 2",
};
var result = VideoListResolver.Resolve(
@@ -222,7 +222,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"My movie #1.mp4",
"My movie #2.mp4"
"My movie #2.mp4",
};
var result = VideoListResolver.Resolve(
@@ -240,7 +240,7 @@ namespace Jellyfin.Naming.Tests.Video
"No (2012) part1.mp4",
"No (2012) part2.mp4",
"No (2012) part1-trailer.mp4",
"No (2012)-trailer.mp4"
"No (2012)-trailer.mp4",
};
var result = VideoListResolver.Resolve(
@@ -261,7 +261,7 @@ namespace Jellyfin.Naming.Tests.Video
"/Movies/Top Gun (1984)/movie.mp4",
"/Movies/Top Gun (1984)/Top Gun (1984)-trailer.mp4",
"/Movies/Top Gun (1984)/Top Gun (1984)-trailer2.mp4",
"/Movies/trailer.mp4"
"/Movies/trailer.mp4",
};
var result = VideoListResolver.Resolve(
@@ -283,7 +283,7 @@ namespace Jellyfin.Naming.Tests.Video
"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd1.avi",
"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Counterfeit Racks (2011) Disc 1 cd2.avi",
"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd1.avi",
"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi"
"/MCFAMILY-PC/Private3$/Heterosexual/Breast In Class 2 Counterfeit Racks (2011)/Breast In Class 2 Disc 2 cd2.avi",
};
var result = VideoListResolver.Resolve(
@@ -298,7 +298,7 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv"
"/nas-markrobbo78/Videos/INDEX HTPC/Movies/Watched/3 - ACTION/Argo (2012)/movie.mkv",
};
var result = VideoListResolver.Resolve(
@@ -313,7 +313,7 @@ namespace Jellyfin.Naming.Tests.Video
{
var files = new[]
{
"The Colony.mkv"
"The Colony.mkv",
};
var result = VideoListResolver.Resolve(
@@ -329,7 +329,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"Four Sisters and a Wedding - A.avi",
"Four Sisters and a Wedding - B.avi"
"Four Sisters and a Wedding - B.avi",
};
var result = VideoListResolver.Resolve(
@@ -347,7 +347,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"Four Rooms - A.avi",
"Four Rooms - A.mp4"
"Four Rooms - A.mp4",
};
var result = VideoListResolver.Resolve(
@@ -363,7 +363,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"/Server/Despicable Me/Despicable Me (2010).mkv",
"/Server/Despicable Me/trailer.mkv"
"/Server/Despicable Me/trailer.mkv",
};
var result = VideoListResolver.Resolve(
@@ -381,7 +381,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"/Server/Despicable Me/Despicable Me (2010).mkv",
"/Server/Despicable Me/trailers/some title.mkv"
"/Server/Despicable Me/trailers/some title.mkv",
};
var result = VideoListResolver.Resolve(
@@ -399,7 +399,7 @@ namespace Jellyfin.Naming.Tests.Video
var files = new[]
{
"/Movies/Despicable Me/Despicable Me.mkv",
"/Movies/Despicable Me/trailers/trailer.mkv"
"/Movies/Despicable Me/trailers/trailer.mkv",
};
var result = VideoListResolver.Resolve(
@@ -416,6 +416,6 @@ namespace Jellyfin.Naming.Tests.Video
{
var stack = new FileStack(string.Empty, false, Array.Empty<string>());
Assert.False(stack.ContainsFile("XX", true));
}
},
}
}
}
@@ -192,7 +192,7 @@ namespace Jellyfin.Naming.Tests.Video
foreach (var result in results)
{
Assert.Null(result?.Container);
}
}
},
},
}
}
}
@@ -1,4 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]
@@ -20,4 +20,3 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Generated by the MSBuild WriteCodeFragment class.
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "aetk2kDndoI=",
"dgSpecHash": "w013NueXjWs=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\tests\\Jellyfin.Naming.Tests\\Jellyfin.Naming.Tests.csproj",
"expectedPackageFiles": [
@@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "VqeAp96kZ+8=",
"dgSpecHash": "dtvsyCwkixc=",
"success": true,
"projectFilePath": "E:\\Projects\\pgsql-jellyfin\\tests\\Jellyfin.Networking.Tests\\Jellyfin.Networking.Tests.csproj",
"expectedPackageFiles": [

Some files were not shown because too many files have changed in this diff Show More