Compare commits

...

17 Commits

Author SHA1 Message Date
Jellyfin Release Bot
1136a36eed Bump version to 10.10.5 2025-01-25 14:14:26 -05:00
gnattu
e8514de33b Don't select audio stream and codec explicitly for copy when bitrate exceeds limit (#13423) 2025-01-25 11:23:51 -07:00
JPVenson
722cdcce5e Add check to prevent downgrade from future EFCore refactor (#13103) 2025-01-25 10:11:19 -07:00
Bond-009
bfe0fdbcdc Open files with FileShare.Read for BlurHash calculations (#13425) 2025-01-25 08:41:51 -07:00
Joshua M. Boniface
0b2a59e963 Merge pull request #13384 from alltilla/fix-parallel-subtitleeditparse
Fix parallel use of not thread-safe SubtitleFormat instance
2025-01-25 02:32:00 -05:00
Joshua M. Boniface
6329de4fc3 Merge pull request #13411 from gnattu/use-writethrough-imagesaver
Use WriteThrough for ImageSaver
2025-01-25 02:29:39 -05:00
gnattu
644df3585b Use WriteThrough for ImageSaver
When writing an image to the disk, we use the completion of the async task as a signal indicating the completion of a write operation. However, this approach may not be entirely accurate, as the operating system can optimize IO operations by writing data to an intermediate cache instead of directly to the disk before completing the operation. This optimization can lead to a data race for our scanner, as subsequent tasks such as blurhash computation may attempt to read a file that has not yet been flushed from the volatile cache. Consequently, the data within the file becomes invalid, causing the blurhash computation task to fail.

Use WriteThrough mode to ensure the data is actual on disk before return to resolve this issue.
2025-01-24 07:54:22 +08:00
Joshua M. Boniface
3766a88bea Merge pull request #13390 from gnattu/catch-ioexception
Catch IOExceptions for GetFileSystemMetadata
2025-01-22 16:36:26 -05:00
Joshua M. Boniface
f333ef74b3 Merge pull request #13092 from TheMelmacian/bugfix/xml_special_characters_in_set_elements
Fix: handling of <set> elements in NfoParser
2025-01-22 16:36:04 -05:00
Joshua M. Boniface
0394965753 Merge pull request #13382 from Shadowghost/fix-published-url-override
Fix interface selection
2025-01-22 16:34:11 -05:00
Joshua M. Boniface
53a45c6033 Merge pull request #13388 from Shadowghost/fix-ratings
Fix rating levels
2025-01-22 16:34:02 -05:00
Shadowghost
adfe52f55a Fix rating levels 2025-01-22 21:32:43 +01:00
Attila Szakacs
c693da94ce Fix parallel use of not thread-safe SubtitleFormat instance
`SubtitleFormat`'s `LoadSubtitle()` function is
not thread-safe.

A `SubtitleEditParser` instance's `Parse()`
function can be called from multiple threads at
the same time.

`SubtitleFormat`s are cached in the constructor
of each `SubtitleEditParser`, and the same
instances are used for each possibly parallel
`Parse()` function call, which causes subtitle
parse problems.

This patch modifies the code, so we only cache
the extension -> `SubtitleFormat` type/class
mapping and create a new `SubtitleFormat`
instance in each `Parse()` call, so no
`SubtitleFormat` instance is accessed from
multiple threads.

Fixes #12113

Kudos for everyone investigating the issue there,
most notably @RenV123 for PoC-ing the solution.

Signed-off-by: Attila Szakacs <szakacs.attila96@gmail.com>
2025-01-18 21:16:35 +01:00
gnattu
1a7c2299c6 Catch IOExceptions for GetFileSystemMetadata
Our `GetFileSystemEntries` method will throw when enumerating the file system, but its callers might consider the unhandled exceptions as the whole path is not available. This would cause a single problematic file to fail the enumeration, and could lead to unexpected side effects.

HandleIOException gracefully by marking the files throwing as not exist to let the caller skip that file.
2025-01-19 00:40:13 +08:00
Shadowghost
9c7d735a96 Fix interface selection 2025-01-17 09:41:32 +01:00
TheMelmacian
5df03b9010 write Kodi conform set element to nfo files 2024-12-01 21:20:51 +01:00
TheMelmacian
ef13a18450 fix(MovieNfoParser): parsing of <set> elements 2024-11-23 11:18:27 +01:00
29 changed files with 134 additions and 65 deletions

View File

@@ -9,8 +9,8 @@
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.3" />
<PackageVersion Include="BlurHashSharp" Version="1.3.3" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
<PackageVersion Include="BlurHashSharp" Version="1.3.4" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Diacritics" Version="3.3.29" />

View File

@@ -36,7 +36,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.10.4</VersionPrefix>
<VersionPrefix>10.10.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -276,6 +276,13 @@ namespace Emby.Server.Implementations.IO
{
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
}
catch (IOException ex)
{
// IOException generally means the file is not accessible due to filesystem issues
// Catch this exception and mark the file as not exist to ignore it
_logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
result.Exists = false;
}
}
}
@@ -590,6 +597,9 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
// Note: any of unhandled exceptions thrown by this method may cause the caller to believe the whole path is not accessible.
// But what causing the exception may be a single file under that path. This could lead to unexpected behavior.
// For example, the scanner will remove everything in that path due to unhandled errors.
var directoryInfo = new DirectoryInfo(path);
var enumerationOptions = GetEnumerationOptions(recursive);

View File

@@ -1,8 +1,14 @@
Livre,0
L,0
ER,9
AL,0
ER,10
10,10
A10,10
12,12
A12,12
14,14
A14,14
16,16
A16,16
18,18
A18,18
1 Livre 0
2 L 0
3 ER AL 9 0
4 ER 10
5 10 10
6 A10 10
7 12 12
8 A12 12
9 14 14
10 A14 14
11 16 16
12 A16 16
13 18 18
14 A18 18

View File

@@ -6,8 +6,6 @@ TV-Y7,7
TV-Y7-FV,7
PG,9
TV-PG,9
PG-13,13
13+,13
TV-14,14
14A,14
16+,16
1 E 0
6 TV-Y7-FV 7
7 PG 9
8 TV-PG 9
PG-13 13
13+ 13
9 TV-14 14
10 14A 14
11 16+ 16

View File

@@ -1,7 +1,7 @@
A,0
A/fig,0
A/i,0
A/fig/i,0
A/i/fig,0
APTA,0
ERI,0
TP,0
1 A 0
2 A/fig 0
3 A/i 0
4 A/fig/i A/i/fig 0
5 APTA 0
6 ERI 0
7 TP 0

View File

@@ -6,10 +6,11 @@ U,0
6+,6
7+,7
PG,8
9+,9
9,9
12,12
12+,12
12A,12
12PG,12
Teen,13
13+,13
14+,14
1 All 0
6 6+ 6
7 7+ 7
8 PG 8
9 9+ 9 9
10 12 12
11 12+ 12
12 12A 12
13 12PG 12
14 Teen 13
15 13+ 13
16 14+ 14

View File

@@ -4,6 +4,7 @@ PG,12
12A,12
12PG,12
15,15
15PG,15
15A,15
16,16
18,18
1 G 4
4 12A 12
5 12PG 12
6 15 15
7 15PG 15
8 15A 15
9 16 16
10 18 18

View File

@@ -6,4 +6,5 @@ A,0
12,12
15,15
18,18
C,18
Not approved,1001
1 A 0
6 12 12
7 15 15
8 18 18
9 C 18
10 Not approved 1001

View File

@@ -10,6 +10,7 @@ R16,16
RP16,16
GA,18
R18,18
RP18,18
MA,1000
R,1001
Objectionable,1001
1 Exempt 0
10 RP16 16
11 GA 18
12 R18 18
13 RP18 18
14 MA 1000
15 R 1001
16 Objectionable 1001

View File

@@ -5,23 +5,23 @@ TV-Y,0
TV-Y7,7
TV-Y7-FV,7
PG,10
TV-PG,10
TV-PG-D,10
TV-PG-L,10
TV-PG-S,10
TV-PG-V,10
TV-PG-DL,10
TV-PG-DS,10
TV-PG-DV,10
TV-PG-LS,10
TV-PG-LV,10
TV-PG-SV,1
TV-PG-DLS,10
TV-PG-DLV,10
TV-PG-DSV,10
TV-PG-LSV,10
TV-PG-DLSV,10
PG-13,13
TV-PG,13
TV-PG-D,13
TV-PG-L,13
TV-PG-S,13
TV-PG-V,13
TV-PG-DL,13
TV-PG-DS,13
TV-PG-DV,13
TV-PG-LS,13
TV-PG-LV,13
TV-PG-SV,13
TV-PG-DLS,13
TV-PG-DLV,13
TV-PG-DSV,13
TV-PG-LSV,13
TV-PG-DLSV,13
TV-14,14
TV-14-D,14
TV-14-L,14
@@ -48,3 +48,5 @@ TV-MA-LS,17
TV-MA-LV,17
TV-MA-SV,17
TV-MA-LSV,17
TV-X,18
TV-AO,18
1 Approved 0
5 TV-Y7 7
6 TV-Y7-FV 7
7 PG 10
8 TV-PG 10
9 TV-PG-D 10
10 TV-PG-L 10
11 TV-PG-S 10
12 TV-PG-V 10
13 TV-PG-DL 10
14 TV-PG-DS 10
15 TV-PG-DV 10
16 TV-PG-LS 10
17 TV-PG-LV 10
18 TV-PG-SV 1
19 TV-PG-DLS 10
20 TV-PG-DLV 10
21 TV-PG-DSV 10
22 TV-PG-LSV 10
23 TV-PG-DLSV 10
24 PG-13 13
TV-PG 13
TV-PG-D 13
TV-PG-L 13
TV-PG-S 13
TV-PG-V 13
TV-PG-DL 13
TV-PG-DS 13
TV-PG-DV 13
TV-PG-LS 13
TV-PG-LV 13
TV-PG-SV 13
TV-PG-DLS 13
TV-PG-DLV 13
TV-PG-DSV 13
TV-PG-LSV 13
TV-PG-DLSV 13
25 TV-14 14
26 TV-14-D 14
27 TV-14-L 14
48 TV-MA-LV 17
49 TV-MA-SV 17
50 TV-MA-LSV 17
51 TV-X 18
52 TV-AO 18

View File

@@ -18,7 +18,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.10.4</VersionPrefix>
<VersionPrefix>10.10.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -51,6 +51,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.RemoveDuplicatePlaylistChildren)
};
private static readonly Guid _downgradeCheckMigration = Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
/// <summary>
/// Run all needed migrations.
/// </summary>
@@ -88,6 +90,12 @@ namespace Jellyfin.Server.Migrations
? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
: new MigrationOptions();
// 10.10 specific EFCore migration check.
if (migrationOptions.Applied.Any(f => f.Id.Equals(_downgradeCheckMigration)))
{
throw new InvalidOperationException("You cannot downgrade your jellyfin install from the library.db migration.");
}
// We have to deserialize it manually since the configuration manager may overwrite it
var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!

View File

@@ -30,7 +30,7 @@ namespace Jellyfin.Server.Migrations.Routines
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}");
public Guid Id => Guid.Parse("{D34BFC33-5D2E-4790-8085-069EF6EECB4E}");
/// <inheritdoc/>
public string Name => "MigrateRatingLevels";

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.10.4</VersionPrefix>
<VersionPrefix>10.10.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.10.4</VersionPrefix>
<VersionPrefix>10.10.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public class SubtitleEditParser : ISubtitleParser
{
private readonly ILogger<SubtitleEditParser> _logger;
private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats;
private readonly Dictionary<string, List<Type>> _subtitleFormatTypes;
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
@@ -26,10 +26,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
{
_logger = logger;
_subtitleFormats = GetSubtitleFormats()
.Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension))
.GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
_subtitleFormatTypes = GetSubtitleFormatTypes();
}
/// <inheritdoc />
@@ -38,13 +35,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var subtitle = new Subtitle();
var lines = stream.ReadAllLines().ToList();
if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats))
if (!_subtitleFormatTypes.TryGetValue(fileExtension, out var subtitleFormatTypesForExtension))
{
throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
}
foreach (var subtitleFormat in subtitleFormats)
foreach (var subtitleFormatType in subtitleFormatTypesForExtension)
{
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(subtitleFormatType, true)!;
_logger.LogDebug(
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
fileExtension,
@@ -97,11 +95,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <inheritdoc />
public bool SupportsFileExtension(string fileExtension)
=> _subtitleFormats.ContainsKey(fileExtension);
=> _subtitleFormatTypes.ContainsKey(fileExtension);
private List<SubtitleFormat> GetSubtitleFormats()
private Dictionary<string, List<Type>> GetSubtitleFormatTypes()
{
var subtitleFormats = new List<SubtitleFormat>();
var subtitleFormatTypes = new Dictionary<string, List<Type>>(StringComparer.OrdinalIgnoreCase);
var assembly = typeof(SubtitleFormat).Assembly;
foreach (var type in assembly.GetTypes())
@@ -113,9 +111,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
// It shouldn't be null, but the exception is caught if it is
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!;
subtitleFormats.Add(subtitleFormat);
var tempInstance = (SubtitleFormat)Activator.CreateInstance(type, true)!;
var extension = tempInstance.Extension.TrimStart('.');
if (!string.IsNullOrEmpty(extension))
{
// Store only the type, we will instantiate from it later
if (!subtitleFormatTypes.TryGetValue(extension, out var subtitleFormatTypesForExtension))
{
subtitleFormatTypes[extension] = [type];
}
else
{
subtitleFormatTypesForExtension.Add(type);
}
}
}
catch (Exception ex)
{
@@ -123,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
return subtitleFormats;
return subtitleFormatTypes;
}
}
}

View File

@@ -1001,6 +1001,8 @@ namespace MediaBrowser.Model.Dlna
}))
.All(satisfied => satisfied);
directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeReason.ContainerBitrateExceedsLimit);
var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId>
<VersionPrefix>10.10.4</VersionPrefix>
<VersionPrefix>10.10.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -291,6 +291,7 @@ namespace MediaBrowser.Providers.Manager
var fileStreamOptions = AsyncFile.WriteOptions;
fileStreamOptions.Mode = FileMode.Create;
fileStreamOptions.Options = FileOptions.WriteThrough;
if (source.CanSeek)
{
fileStreamOptions.PreallocationSize = source.Length;

View File

@@ -79,21 +79,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val) && movie is not null)
{
// TODO Handle this better later
if (!val.Contains('<', StringComparison.Ordinal))
try
{
movie.CollectionName = val;
ParseSetXml(val, movie);
}
else
catch (Exception ex)
{
try
{
ParseSetXml(val, movie);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error parsing set node");
}
Logger.LogError(ex, "Error parsing set node");
}
}
@@ -136,7 +128,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
if (reader.NodeType == XmlNodeType.Element)
if (reader.NodeType == XmlNodeType.Text && reader.Depth == 1)
{
movie.CollectionName = reader.Value;
break;
}
else if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{

View File

@@ -115,7 +115,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
if (!string.IsNullOrEmpty(movie.CollectionName))
{
writer.WriteElementString("set", movie.CollectionName);
writer.WriteStartElement("set");
writer.WriteElementString("name", movie.CollectionName);
writer.WriteEndElement();
}
}
}

View File

@@ -1,4 +1,4 @@
using System.Reflection;
[assembly: AssemblyVersion("10.10.4")]
[assembly: AssemblyFileVersion("10.10.4")]
[assembly: AssemblyVersion("10.10.5")]
[assembly: AssemblyFileVersion("10.10.5")]

View File

@@ -195,8 +195,10 @@ public class SkiaEncoder : IImageEncoder
return string.Empty;
}
// Use FileStream with FileShare.Read instead of having Skia open the file to allow concurrent read access
using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
// Any larger than 128x128 is too slow and there's no visually discernible difference
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128);
}
private bool RequiresSpecialCharacterHack(string path)

View File

@@ -15,7 +15,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId>
<VersionPrefix>10.10.4</VersionPrefix>
<VersionPrefix>10.10.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -997,7 +997,9 @@ public class NetworkManager : INetworkManager, IDisposable
// Get interface matching override subnet
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
if (intf?.Address is not null)
if (intf?.Address is not null
|| (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
|| (data.Data.AddressFamily == AddressFamily.InterNetworkV6 && data.Data.Address.Equals(IPAddress.IPv6Any)))
{
// If matching interface is found, use override
bindPreference = data.OverrideUri;
@@ -1025,6 +1027,7 @@ public class NetworkManager : INetworkManager, IDisposable
}
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
return true;
}
@@ -1062,7 +1065,7 @@ public class NetworkManager : INetworkManager, IDisposable
// Check to see if any of the external bind interfaces are in the same subnet as the source.
// If none exists, this will select the first external interface if there is one.
bindAddress = externalInterfaces
.OrderByDescending(x => x.Subnet.Contains(source))
.OrderBy(x => x.Subnet.Contains(source))
.ThenBy(x => x.Index)
.Select(x => x.Address)
.First();
@@ -1079,7 +1082,7 @@ public class NetworkManager : INetworkManager, IDisposable
// Check to see if any of the internal bind interfaces are in the same subnet as the source.
// If none exists, this will select the first internal interface if there is one.
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
.OrderByDescending(x => x.Subnet.Contains(source))
.OrderBy(x => x.Subnet.Contains(source))
.ThenBy(x => x.Index)
.Select(x => x.Address)
.FirstOrDefault();

View File

@@ -84,7 +84,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
Assert.Equal(54, ratings.Count);
Assert.Equal(56, ratings.Count);
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
Assert.NotNull(tvma);

View File

@@ -257,5 +257,23 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
}
[Fact]
public void Parsing_Fields_With_Escaped_Xml_Special_Characters_Success()
{
var result = new MetadataResult<Video>()
{
Item = new Movie()
};
_parser.Fetch(result, "Test Data/Lilo & Stitch.nfo", CancellationToken.None);
var item = (Movie)result.Item;
Assert.Equal("Lilo & Stitch", item.Name);
Assert.Equal("Lilo & Stitch", item.OriginalTitle);
Assert.Equal("Lilo & Stitch Collection", item.CollectionName);
Assert.StartsWith(">>", item.Overview, StringComparison.InvariantCulture);
Assert.EndsWith("<<", item.Overview, StringComparison.InvariantCulture);
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<movie>
<title>Lilo &amp; Stitch</title>
<originaltitle>Lilo &amp; Stitch</originaltitle>
<set>Lilo &amp; Stitch Collection</set>
<plot>&gt;&gt;As Stitch, a runaway genetic experiment from a faraway planet, wreaks havoc on the Hawaiian Islands, he becomes the mischievous adopted alien "puppy" of an independent little girl named Lilo and learns about loyalty, friendship, and ʻohana, the Hawaiian tradition of family.&lt;&lt;</plot>
</movie>