mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-06-03 21:54:26 -04:00
Merge branch 'master' into fix-hwa-video-rotation
This commit is contained in:
commit
2aa9cf4007
2
.github/ISSUE_TEMPLATE/issue report.yml
vendored
2
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@ -86,7 +86,7 @@ body:
|
|||||||
label: Jellyfin Server version
|
label: Jellyfin Server version
|
||||||
description: What version of Jellyfin are you using?
|
description: What version of Jellyfin are you using?
|
||||||
options:
|
options:
|
||||||
- 10.9.7
|
- 10.9.8+
|
||||||
- Master
|
- Master
|
||||||
- Unstable
|
- Unstable
|
||||||
- Older*
|
- Older*
|
||||||
|
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
@ -27,11 +27,11 @@ jobs:
|
|||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
|
uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
|
uses: github/codeql-action/autobuild@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
|
uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
||||||
|
4
.github/workflows/ci-openapi.yml
vendored
4
.github/workflows/ci-openapi.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
|||||||
- name: Generate openapi.json
|
- name: Generate openapi.json
|
||||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||||
- name: Upload openapi.json
|
- name: Upload openapi.json
|
||||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||||
with:
|
with:
|
||||||
name: openapi-head
|
name: openapi-head
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@ -61,7 +61,7 @@ jobs:
|
|||||||
- name: Generate openapi.json
|
- name: Generate openapi.json
|
||||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||||
- name: Upload openapi.json
|
- name: Upload openapi.json
|
||||||
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
|
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||||
with:
|
with:
|
||||||
name: openapi-base
|
name: openapi-base
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
@ -185,6 +185,7 @@
|
|||||||
- [Vedant](https://github.com/viktory36/)
|
- [Vedant](https://github.com/viktory36/)
|
||||||
- [NotSaifA](https://github.com/NotSaifA)
|
- [NotSaifA](https://github.com/NotSaifA)
|
||||||
- [HonestlyWhoKnows](https://github.com/honestlywhoknows)
|
- [HonestlyWhoKnows](https://github.com/honestlywhoknows)
|
||||||
|
- [TheMelmacian](https://github.com/TheMelmacian)
|
||||||
- [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode)
|
- [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||||
<ItemGroup Label="Package Dependencies">
|
<ItemGroup Label="Package Dependencies">
|
||||||
<PackageVersion Include="AsyncKeyedLock" Version="6.4.2" />
|
<PackageVersion Include="AsyncKeyedLock" Version="7.0.0" />
|
||||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||||
@ -22,7 +22,7 @@
|
|||||||
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
|
||||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
|
||||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||||
<PackageVersion Include="libse" Version="4.0.5" />
|
<PackageVersion Include="libse" Version="4.0.7" />
|
||||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
|
||||||
@ -58,7 +58,7 @@
|
|||||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
||||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
|
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
|
||||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
|
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||||
@ -72,7 +72,7 @@
|
|||||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
|
||||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||||
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
|
<PackageVersion Include="Svg.Skia" Version="2.0.0" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||||
@ -81,6 +81,7 @@
|
|||||||
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
|
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
|
||||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
|
||||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||||
|
<PackageVersion Include="z440.atl.core" Version="5.25.0" />
|
||||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||||
|
@ -24,6 +24,8 @@ namespace Emby.Naming.TV
|
|||||||
"stagione"
|
"stagione"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly char[] _splitChars = ['.', '_', ' ', '-'];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to parse season number from path.
|
/// Attempts to parse season number from path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -83,14 +85,9 @@ namespace Emby.Naming.TV
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase))
|
if (TryGetSeasonNumberFromPart(filename, out int seasonNumber))
|
||||||
{
|
{
|
||||||
var testFilename = filename.AsSpan().Slice(1);
|
return (seasonNumber, true);
|
||||||
|
|
||||||
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
|
||||||
{
|
|
||||||
return (val, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for one of the season folder names
|
// Look for one of the season folder names
|
||||||
@ -108,10 +105,10 @@ namespace Emby.Naming.TV
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
|
var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries);
|
||||||
foreach (var part in parts)
|
foreach (var part in parts)
|
||||||
{
|
{
|
||||||
if (TryGetSeasonNumberFromPart(part, out int seasonNumber))
|
if (TryGetSeasonNumberFromPart(part, out seasonNumber))
|
||||||
{
|
{
|
||||||
return (seasonNumber, true);
|
return (seasonNumber, true);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
|
|||||||
private readonly ILogger<PhotoProvider> _logger;
|
private readonly ILogger<PhotoProvider> _logger;
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
|
|
||||||
// These are causing taglib to hang
|
// Other extensions might cause taglib to hang
|
||||||
private readonly string[] _includeExtensions = [".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif"];
|
private readonly string[] _includeExtensions = [".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif"];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -333,10 +333,10 @@ namespace Emby.Server.Implementations.Data
|
|||||||
/// <returns>The user item data.</returns>
|
/// <returns>The user item data.</returns>
|
||||||
private UserItemData ReadRow(SqliteDataReader reader)
|
private UserItemData ReadRow(SqliteDataReader reader)
|
||||||
{
|
{
|
||||||
var userData = new UserItemData();
|
var userData = new UserItemData
|
||||||
|
{
|
||||||
userData.Key = reader[0].ToString();
|
Key = reader.GetString(0)
|
||||||
// userData.UserId = reader[1].ReadGuidFromBlob();
|
};
|
||||||
|
|
||||||
if (reader.TryGetDouble(2, out var rating))
|
if (reader.TryGetDouble(2, out var rating))
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -83,12 +81,12 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
|
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null)
|
||||||
{
|
{
|
||||||
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
|
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
|
||||||
var returnItems = new BaseItemDto[accessibleItems.Count];
|
var returnItems = new BaseItemDto[accessibleItems.Count];
|
||||||
List<(BaseItem, BaseItemDto)> programTuples = null;
|
List<(BaseItem, BaseItemDto)>? programTuples = null;
|
||||||
List<(BaseItemDto, LiveTvChannel)> channelTuples = null;
|
List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
|
||||||
|
|
||||||
for (int index = 0; index < accessibleItems.Count; index++)
|
for (int index = 0; index < accessibleItems.Count; index++)
|
||||||
{
|
{
|
||||||
@ -137,7 +135,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return returnItems;
|
return returnItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null)
|
public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
|
||||||
{
|
{
|
||||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
||||||
if (item is LiveTvChannel tvChannel)
|
if (item is LiveTvChannel tvChannel)
|
||||||
@ -167,7 +165,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IList<BaseItem> GetTaggedItems(IItemByName byName, User user, DtoOptions options)
|
private static IList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
|
||||||
{
|
{
|
||||||
return byName.GetTaggedItems(
|
return byName.GetTaggedItems(
|
||||||
new InternalItemsQuery(user)
|
new InternalItemsQuery(user)
|
||||||
@ -177,7 +175,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null)
|
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
|
||||||
{
|
{
|
||||||
var dto = new BaseItemDto
|
var dto = new BaseItemDto
|
||||||
{
|
{
|
||||||
@ -292,7 +290,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
}
|
}
|
||||||
|
|
||||||
var path = mediaSource.Path;
|
var path = mediaSource.Path;
|
||||||
string fileExtensionContainer = null;
|
string? fileExtensionContainer = null;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(path))
|
if (!string.IsNullOrEmpty(path))
|
||||||
{
|
{
|
||||||
@ -316,7 +314,8 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null)
|
/// <inheritdoc />
|
||||||
|
public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user = null)
|
||||||
{
|
{
|
||||||
var dto = GetBaseItemDtoInternal(item, options, user);
|
var dto = GetBaseItemDtoInternal(item, options, user);
|
||||||
|
|
||||||
@ -486,10 +485,10 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return images
|
return images
|
||||||
.Select(p => GetImageCacheTag(item, p))
|
.Select(p => GetImageCacheTag(item, p))
|
||||||
.Where(i => i is not null)
|
.Where(i => i is not null)
|
||||||
.ToArray();
|
.ToArray()!; // null values got filtered out
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
|
private string? GetImageCacheTag(BaseItem item, ItemImageInfo image)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -508,7 +507,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
/// <param name="dto">The dto.</param>
|
/// <param name="dto">The dto.</param>
|
||||||
/// <param name="item">The item.</param>
|
/// <param name="item">The item.</param>
|
||||||
/// <param name="user">The requesting user.</param>
|
/// <param name="user">The requesting user.</param>
|
||||||
private void AttachPeople(BaseItemDto dto, BaseItem item, User user = null)
|
private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null)
|
||||||
{
|
{
|
||||||
// Ordering by person type to ensure actors and artists are at the front.
|
// Ordering by person type to ensure actors and artists are at the front.
|
||||||
// This is taking advantage of the fact that they both begin with A
|
// This is taking advantage of the fact that they both begin with A
|
||||||
@ -552,7 +551,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
|
|
||||||
var list = new List<BaseItemPerson>();
|
var list = new List<BaseItemPerson>();
|
||||||
|
|
||||||
var dictionary = people.Select(p => p.Name)
|
Dictionary<string, Person> dictionary = people.Select(p => p.Name)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase).Select(c =>
|
.Distinct(StringComparer.OrdinalIgnoreCase).Select(c =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -565,9 +564,9 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}).Where(i => i is not null)
|
}).Where(i => i is not null)
|
||||||
.Where(i => user is null || i.IsVisible(user))
|
.Where(i => user is null || i!.IsVisible(user))
|
||||||
.DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
|
.DistinctBy(x => x!.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(i => i!.Name, StringComparer.OrdinalIgnoreCase)!; // null values got filtered out
|
||||||
|
|
||||||
for (var i = 0; i < people.Count; i++)
|
for (var i = 0; i < people.Count; i++)
|
||||||
{
|
{
|
||||||
@ -580,7 +579,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
Type = person.Type
|
Type = person.Type
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dictionary.TryGetValue(person.Name, out Person entity))
|
if (dictionary.TryGetValue(person.Name, out Person? entity))
|
||||||
{
|
{
|
||||||
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
|
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
|
||||||
baseItemPerson.Id = entity.Id;
|
baseItemPerson.Id = entity.Id;
|
||||||
@ -650,7 +649,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return _libraryManager.GetGenreId(name);
|
return _libraryManager.GetGenreId(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
|
private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
|
||||||
{
|
{
|
||||||
var image = item.GetImageInfo(imageType, imageIndex);
|
var image = item.GetImageInfo(imageType, imageIndex);
|
||||||
if (image is not null)
|
if (image is not null)
|
||||||
@ -661,9 +660,14 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
|
private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
|
||||||
{
|
{
|
||||||
var tag = GetImageCacheTag(item, image);
|
var tag = GetImageCacheTag(item, image);
|
||||||
|
if (tag is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(image.BlurHash))
|
if (!string.IsNullOrEmpty(image.BlurHash))
|
||||||
{
|
{
|
||||||
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
|
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
|
||||||
@ -716,7 +720,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
/// <param name="item">The item.</param>
|
/// <param name="item">The item.</param>
|
||||||
/// <param name="owner">The owner.</param>
|
/// <param name="owner">The owner.</param>
|
||||||
/// <param name="options">The options.</param>
|
/// <param name="options">The options.</param>
|
||||||
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem owner, DtoOptions options)
|
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options)
|
||||||
{
|
{
|
||||||
if (options.ContainsField(ItemFields.DateCreated))
|
if (options.ContainsField(ItemFields.DateCreated))
|
||||||
{
|
{
|
||||||
@ -1097,7 +1101,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseItem[] allExtras = null;
|
BaseItem[]? allExtras = null;
|
||||||
|
|
||||||
if (options.ContainsField(ItemFields.SpecialFeatureCount))
|
if (options.ContainsField(ItemFields.SpecialFeatureCount))
|
||||||
{
|
{
|
||||||
@ -1134,7 +1138,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
dto.SeasonId = episode.SeasonId;
|
dto.SeasonId = episode.SeasonId;
|
||||||
dto.SeriesId = episode.SeriesId;
|
dto.SeriesId = episode.SeriesId;
|
||||||
|
|
||||||
Series episodeSeries = null;
|
Series? episodeSeries = null;
|
||||||
|
|
||||||
// this block will add the series poster for episodes without a poster
|
// this block will add the series poster for episodes without a poster
|
||||||
// TODO maybe remove the if statement entirely
|
// TODO maybe remove the if statement entirely
|
||||||
@ -1162,8 +1166,10 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add SeriesInfo
|
// Add SeriesInfo
|
||||||
if (item is Series series)
|
Series? series;
|
||||||
|
if (item is Series tmp)
|
||||||
{
|
{
|
||||||
|
series = tmp;
|
||||||
dto.AirDays = series.AirDays;
|
dto.AirDays = series.AirDays;
|
||||||
dto.AirTime = series.AirTime;
|
dto.AirTime = series.AirTime;
|
||||||
dto.Status = series.Status?.ToString();
|
dto.Status = series.Status?.ToString();
|
||||||
@ -1264,7 +1270,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseItem GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem)
|
private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem)
|
||||||
{
|
{
|
||||||
if (currentItem is MusicAlbum musicAlbum)
|
if (currentItem is MusicAlbum musicAlbum)
|
||||||
{
|
{
|
||||||
@ -1285,7 +1291,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem owner)
|
private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner)
|
||||||
{
|
{
|
||||||
if (!item.SupportsInheritedParentImages)
|
if (!item.SupportsInheritedParentImages)
|
||||||
{
|
{
|
||||||
@ -1305,7 +1311,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseItem parent = null;
|
BaseItem? parent = null;
|
||||||
var isFirst = true;
|
var isFirst = true;
|
||||||
|
|
||||||
var imageTags = dto.ImageTags;
|
var imageTags = dto.ImageTags;
|
||||||
@ -1378,7 +1384,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetMappedPath(BaseItem item, BaseItem ownerItem)
|
private string GetMappedPath(BaseItem item, BaseItem? ownerItem)
|
||||||
{
|
{
|
||||||
var path = item.Path;
|
var path = item.Path;
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -138,13 +137,13 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
|
|
||||||
return new UserDataChangeInfo
|
return new UserDataChangeInfo
|
||||||
{
|
{
|
||||||
UserId = userId.ToString("N", CultureInfo.InvariantCulture),
|
UserId = userId,
|
||||||
UserDataList = changedItems
|
UserDataList = changedItems
|
||||||
.DistinctBy(x => x.Id)
|
.DistinctBy(x => x.Id)
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
var dto = _userDataManager.GetUserDataDto(i, user);
|
var dto = _userDataManager.GetUserDataDto(i, user);
|
||||||
dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture);
|
dto.ItemId = i.Id;
|
||||||
return dto;
|
return dto;
|
||||||
})
|
})
|
||||||
.ToArray()
|
.ToArray()
|
||||||
|
@ -379,7 +379,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
|
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
|
||||||
{
|
{
|
||||||
if (userData.SubtitleStreamIndex.HasValue
|
if (userData is not null
|
||||||
|
&& userData.SubtitleStreamIndex.HasValue
|
||||||
&& user.RememberSubtitleSelections
|
&& user.RememberSubtitleSelections
|
||||||
&& user.SubtitleMode != SubtitlePlaybackMode.None
|
&& user.SubtitleMode != SubtitlePlaybackMode.None
|
||||||
&& allowRememberingSelection)
|
&& allowRememberingSelection)
|
||||||
@ -411,7 +412,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
|
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
|
||||||
{
|
{
|
||||||
if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
|
if (userData is not null && userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
|
||||||
{
|
{
|
||||||
var index = userData.AudioStreamIndex.Value;
|
var index = userData.AudioStreamIndex.Value;
|
||||||
// Make sure the saved index is still valid
|
// Make sure the saved index is still valid
|
||||||
@ -434,7 +435,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
if (mediaType == MediaType.Video)
|
if (mediaType == MediaType.Video)
|
||||||
{
|
{
|
||||||
var userData = item is null ? new UserItemData() : _userDataManager.GetUserData(user, item);
|
var userData = item is null ? null : _userDataManager.GetUserData(user, item);
|
||||||
|
|
||||||
var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections;
|
var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections;
|
||||||
|
|
||||||
|
@ -68,11 +68,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
var justName = Path.GetFileName(item.Path.AsSpan());
|
var justName = Path.GetFileName(item.Path.AsSpan());
|
||||||
|
|
||||||
var id = justName.GetAttributeValue("tmdbid");
|
var id = justName.GetAttributeValue("tmdbid");
|
||||||
|
item.TrySetProviderId(MetadataProvider.Tmdb, id);
|
||||||
if (!string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Tmdb, id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -373,22 +373,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
{
|
{
|
||||||
// Check for TMDb id
|
// Check for TMDb id
|
||||||
var tmdbid = justName.GetAttributeValue("tmdbid");
|
var tmdbid = justName.GetAttributeValue("tmdbid");
|
||||||
|
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
|
||||||
if (!string.IsNullOrWhiteSpace(tmdbid))
|
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Tmdb, tmdbid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(item.Path))
|
if (!string.IsNullOrEmpty(item.Path))
|
||||||
{
|
{
|
||||||
// Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)
|
// Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)
|
||||||
var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
|
var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
|
||||||
|
item.TrySetProviderId(MetadataProvider.Imdb, imdbid);
|
||||||
if (!string.IsNullOrWhiteSpace(imdbid))
|
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Imdb, imdbid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,46 +186,25 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
var justName = Path.GetFileName(path.AsSpan());
|
var justName = Path.GetFileName(path.AsSpan());
|
||||||
|
|
||||||
var imdbId = justName.GetAttributeValue("imdbid");
|
var imdbId = justName.GetAttributeValue("imdbid");
|
||||||
if (!string.IsNullOrEmpty(imdbId))
|
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Imdb, imdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var tvdbId = justName.GetAttributeValue("tvdbid");
|
var tvdbId = justName.GetAttributeValue("tvdbid");
|
||||||
if (!string.IsNullOrEmpty(tvdbId))
|
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Tvdb, tvdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var tvmazeId = justName.GetAttributeValue("tvmazeid");
|
var tvmazeId = justName.GetAttributeValue("tvmazeid");
|
||||||
if (!string.IsNullOrEmpty(tvmazeId))
|
item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId);
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.TvMaze, tvmazeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var tmdbId = justName.GetAttributeValue("tmdbid");
|
var tmdbId = justName.GetAttributeValue("tmdbid");
|
||||||
if (!string.IsNullOrEmpty(tmdbId))
|
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Tmdb, tmdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var anidbId = justName.GetAttributeValue("anidbid");
|
var anidbId = justName.GetAttributeValue("anidbid");
|
||||||
if (!string.IsNullOrEmpty(anidbId))
|
item.TrySetProviderId("AniDB", anidbId);
|
||||||
{
|
|
||||||
item.SetProviderId("AniDB", anidbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var aniListId = justName.GetAttributeValue("anilistid");
|
var aniListId = justName.GetAttributeValue("anilistid");
|
||||||
if (!string.IsNullOrEmpty(aniListId))
|
item.TrySetProviderId("AniList", aniListId);
|
||||||
{
|
|
||||||
item.SetProviderId("AniList", aniListId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var aniSearchId = justName.GetAttributeValue("anisearchid");
|
var aniSearchId = justName.GetAttributeValue("anisearchid");
|
||||||
if (!string.IsNullOrEmpty(aniSearchId))
|
item.TrySetProviderId("AniSearch", aniSearchId);
|
||||||
{
|
|
||||||
item.SetProviderId("AniSearch", aniSearchId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,5 +124,11 @@
|
|||||||
"External": "Externo",
|
"External": "Externo",
|
||||||
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
|
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
|
||||||
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
|
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
|
||||||
"HearingImpaired": "Discapacidad Auditiva"
|
"HearingImpaired": "Discapacidad Auditiva",
|
||||||
|
"TaskRefreshTrickplayImages": "Generar imágenes de Trickplay",
|
||||||
|
"TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reproducción engañosa para videos en bibliotecas habilitadas.",
|
||||||
|
"TaskAudioNormalization": "Normalización de audio",
|
||||||
|
"TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de normalización de audio.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen."
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
"HeaderLiveTV": "Live TV",
|
"HeaderLiveTV": "Live TV",
|
||||||
"HeaderFavoriteSongs": "Mga Paboritong Kanta",
|
"HeaderFavoriteSongs": "Mga Paboritong Kanta",
|
||||||
"HeaderFavoriteShows": "Mga Paboritong Pelikula",
|
"HeaderFavoriteShows": "Mga Paboritong Pelikula",
|
||||||
"HeaderFavoriteEpisodes": "Mga Paboritong Episode",
|
"HeaderFavoriteEpisodes": "Mga Paboritong Yugto",
|
||||||
"HeaderFavoriteArtists": "Mga Paboritong Artista",
|
"HeaderFavoriteArtists": "Mga Paboritong Artista",
|
||||||
"HeaderFavoriteAlbums": "Mga Paboritong Album",
|
"HeaderFavoriteAlbums": "Mga Paboritong Album",
|
||||||
"HeaderContinueWatching": "Magpatuloy sa Panonood",
|
"HeaderContinueWatching": "Magpatuloy sa Panonood",
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"Collections": "Kolekcije",
|
"Collections": "Kolekcije",
|
||||||
"DeviceOfflineWithName": "{0} je prekinuo vezu",
|
"DeviceOfflineWithName": "{0} je prekinuo vezu",
|
||||||
"DeviceOnlineWithName": "{0} je povezan",
|
"DeviceOnlineWithName": "{0} je povezan",
|
||||||
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}",
|
"FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}",
|
||||||
"Favorites": "Favoriti",
|
"Favorites": "Favoriti",
|
||||||
"Folders": "Mape",
|
"Folders": "Mape",
|
||||||
"Genres": "Žanrovi",
|
"Genres": "Žanrovi",
|
||||||
@ -127,5 +127,8 @@
|
|||||||
"HearingImpaired": "Oštećen sluh",
|
"HearingImpaired": "Oštećen sluh",
|
||||||
"TaskRefreshTrickplayImages": "Generiraj Trickplay Slike",
|
"TaskRefreshTrickplayImages": "Generiraj Trickplay Slike",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama.",
|
"TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama.",
|
||||||
"TaskAudioNormalization": "Normalizacija zvuka"
|
"TaskAudioNormalization": "Normalizacija zvuka",
|
||||||
|
"TaskAudioNormalizationDescription": "Skenira datoteke u potrazi za podacima o normalizaciji zvuka.",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz zbirki i popisa za reprodukciju koje više ne postoje.",
|
||||||
|
"TaskCleanCollectionsAndPlaylists": "Očisti zbirke i popise za reprodukciju"
|
||||||
}
|
}
|
||||||
|
@ -125,5 +125,10 @@
|
|||||||
"TaskKeyframeExtractor": "키프레임 추출",
|
"TaskKeyframeExtractor": "키프레임 추출",
|
||||||
"External": "외부",
|
"External": "외부",
|
||||||
"HearingImpaired": "청각 장애",
|
"HearingImpaired": "청각 장애",
|
||||||
"TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리"
|
"TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리",
|
||||||
|
"TaskAudioNormalization": "오디오의 볼륨 수준을 일정하게 조정",
|
||||||
|
"TaskAudioNormalizationDescription": "오디오의 볼륨 수준을 일정하게 조정하기 위해 파일을 스캔합니다.",
|
||||||
|
"TaskRefreshTrickplayImages": "비디오 탐색용 미리보기 썸네일 생성",
|
||||||
|
"TaskRefreshTrickplayImagesDescription": "활성화된 라이브러리에서 비디오의 트릭플레이 미리보기를 생성합니다.",
|
||||||
|
"TaskCleanCollectionsAndPlaylistsDescription": "더 이상 존재하지 않는 컬렉션 및 재생 목록에서 항목을 제거합니다."
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
"Genres": "Žanri",
|
"Genres": "Žanri",
|
||||||
"Folders": "Mapes",
|
"Folders": "Mapes",
|
||||||
"Favorites": "Izlase",
|
"Favorites": "Izlase",
|
||||||
"FailedLoginAttemptWithUserName": "Neveiksmīgs ielogošanos mēģinājums no {0}",
|
"FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
|
||||||
"DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
|
"DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
|
||||||
"DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
|
"DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
|
||||||
"Collections": "Kolekcijas",
|
"Collections": "Kolekcijas",
|
||||||
@ -95,7 +95,7 @@
|
|||||||
"TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
|
"TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
|
||||||
"TasksApplicationCategory": "Lietotne",
|
"TasksApplicationCategory": "Lietotne",
|
||||||
"TasksLibraryCategory": "Bibliotēka",
|
"TasksLibraryCategory": "Bibliotēka",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "Meklē trūkstošus subtitrus internēta balstoties uz metadatu uzstādījumiem.",
|
"TaskDownloadMissingSubtitlesDescription": "Meklē internetā trūkstošos subtitrus, pamatojoties uz metadatu konfigurāciju.",
|
||||||
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
|
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
|
||||||
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
|
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
|
||||||
"TaskRefreshChannels": "Atjaunot kanālus",
|
"TaskRefreshChannels": "Atjaunot kanālus",
|
||||||
@ -127,7 +127,7 @@
|
|||||||
"TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus",
|
"TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās.",
|
"TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās.",
|
||||||
"TaskAudioNormalization": "Audio normalizācija",
|
"TaskAudioNormalization": "Audio normalizācija",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Noņem elemēntus no kolekcijām un atskaņošanas sarakstiem, kuri vairs neeksistē.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Noņem vairs neeksistējošus vienumus no kolekcijām un atskaņošanas sarakstiem.",
|
||||||
"TaskAudioNormalizationDescription": "Skanē failus priekš audio normālizācijas informācijas.",
|
"TaskAudioNormalizationDescription": "Skanē failus priekš audio normālizācijas informācijas.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Notīrīt kolekcijas un atskaņošanas sarakstus"
|
"TaskCleanCollectionsAndPlaylists": "Notīrīt kolekcijas un atskaņošanas sarakstus"
|
||||||
}
|
}
|
||||||
|
@ -1733,7 +1733,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
|
|
||||||
var channels = state.OutputAudioChannels;
|
var channels = state.OutputAudioChannels;
|
||||||
|
|
||||||
var useDownMixAlgorithm = state.AudioStream.Channels is 6 && _encodingOptions.DownMixStereoAlgorithm != DownMixStereoAlgorithms.None;
|
var useDownMixAlgorithm = DownMixAlgorithmsHelper.AlgorithmFilterStrings.ContainsKey((_encodingOptions.DownMixStereoAlgorithm, DownMixAlgorithmsHelper.InferChannelLayout(state.AudioStream)));
|
||||||
|
|
||||||
if (channels.HasValue
|
if (channels.HasValue
|
||||||
&& (channels.Value != 2
|
&& (channels.Value != 2
|
||||||
|
@ -656,7 +656,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
var query = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
ChannelIds = body.ChannelIds,
|
ChannelIds = body.ChannelIds ?? [],
|
||||||
HasAired = body.HasAired,
|
HasAired = body.HasAired,
|
||||||
IsAiring = body.IsAiring,
|
IsAiring = body.IsAiring,
|
||||||
EnableTotalRecordCount = body.EnableTotalRecordCount,
|
EnableTotalRecordCount = body.EnableTotalRecordCount,
|
||||||
@ -666,31 +666,31 @@ public class LiveTvController : BaseJellyfinApiController
|
|||||||
MaxEndDate = body.MaxEndDate,
|
MaxEndDate = body.MaxEndDate,
|
||||||
StartIndex = body.StartIndex,
|
StartIndex = body.StartIndex,
|
||||||
Limit = body.Limit,
|
Limit = body.Limit,
|
||||||
OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder),
|
OrderBy = RequestHelpers.GetOrderBy(body.SortBy ?? [], body.SortOrder ?? []),
|
||||||
IsNews = body.IsNews,
|
IsNews = body.IsNews,
|
||||||
IsMovie = body.IsMovie,
|
IsMovie = body.IsMovie,
|
||||||
IsSeries = body.IsSeries,
|
IsSeries = body.IsSeries,
|
||||||
IsKids = body.IsKids,
|
IsKids = body.IsKids,
|
||||||
IsSports = body.IsSports,
|
IsSports = body.IsSports,
|
||||||
SeriesTimerId = body.SeriesTimerId,
|
SeriesTimerId = body.SeriesTimerId,
|
||||||
Genres = body.Genres,
|
Genres = body.Genres ?? [],
|
||||||
GenreIds = body.GenreIds
|
GenreIds = body.GenreIds ?? []
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!body.LibrarySeriesId.IsEmpty())
|
if (!body.LibrarySeriesId.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
query.IsSeries = true;
|
query.IsSeries = true;
|
||||||
|
|
||||||
var series = _libraryManager.GetItemById<Series>(body.LibrarySeriesId);
|
var series = _libraryManager.GetItemById<Series>(body.LibrarySeriesId.Value);
|
||||||
if (series is not null)
|
if (series is not null)
|
||||||
{
|
{
|
||||||
query.Name = series.Name;
|
query.Name = series.Name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions { Fields = body.Fields }
|
var dtoOptions = new DtoOptions { Fields = body.Fields ?? [] }
|
||||||
.AddClientFields(User)
|
.AddClientFields(User)
|
||||||
.AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
|
.AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes ?? []);
|
||||||
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,8 +26,6 @@ public static class DtoExtensions
|
|||||||
internal static DtoOptions AddClientFields(
|
internal static DtoOptions AddClientFields(
|
||||||
this DtoOptions dtoOptions, ClaimsPrincipal user)
|
this DtoOptions dtoOptions, ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
dtoOptions.Fields ??= Array.Empty<ItemFields>();
|
|
||||||
|
|
||||||
string? client = user.GetClient();
|
string? client = user.GetClient();
|
||||||
|
|
||||||
// No client in claim
|
// No client in claim
|
||||||
|
@ -38,7 +38,7 @@ public static class FileStreamResponseHelpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Can't dispose the response as it's required up the call chain.
|
// Can't dispose the response as it's required up the call chain.
|
||||||
var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
|
var response = await httpClient.GetAsync(new Uri(state.MediaPath), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||||
var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
|
var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
|
||||||
|
|
||||||
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions.Json.Converters;
|
using Jellyfin.Extensions.Json.Converters;
|
||||||
@ -17,7 +18,7 @@ public class GetProgramsDto
|
|||||||
/// Gets or sets the channels to return guide information for.
|
/// Gets or sets the channels to return guide information for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||||
public IReadOnlyList<Guid> ChannelIds { get; set; } = Array.Empty<Guid>();
|
public IReadOnlyList<Guid>? ChannelIds { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets optional. Filter by user id.
|
/// Gets or sets optional. Filter by user id.
|
||||||
@ -26,153 +27,133 @@ public class GetProgramsDto
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the minimum premiere start date.
|
/// Gets or sets the minimum premiere start date.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? MinStartDate { get; set; }
|
public DateTime? MinStartDate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets filter by programs that have completed airing, or not.
|
/// Gets or sets filter by programs that have completed airing, or not.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? HasAired { get; set; }
|
public bool? HasAired { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets filter by programs that are currently airing, or not.
|
/// Gets or sets filter by programs that are currently airing, or not.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? IsAiring { get; set; }
|
public bool? IsAiring { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the maximum premiere start date.
|
/// Gets or sets the maximum premiere start date.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? MaxStartDate { get; set; }
|
public DateTime? MaxStartDate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the minimum premiere end date.
|
/// Gets or sets the minimum premiere end date.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? MinEndDate { get; set; }
|
public DateTime? MinEndDate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the maximum premiere end date.
|
/// Gets or sets the maximum premiere end date.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? MaxEndDate { get; set; }
|
public DateTime? MaxEndDate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets filter for movies.
|
/// Gets or sets filter for movies.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? IsMovie { get; set; }
|
public bool? IsMovie { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets filter for series.
|
/// Gets or sets filter for series.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? IsSeries { get; set; }
|
public bool? IsSeries { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets filter for news.
|
/// Gets or sets filter for news.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? IsNews { get; set; }
|
public bool? IsNews { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets filter for kids.
|
/// Gets or sets filter for kids.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? IsKids { get; set; }
|
public bool? IsKids { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets filter for sports.
|
/// Gets or sets filter for sports.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? IsSports { get; set; }
|
public bool? IsSports { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the record index to start at. All items with a lower index will be dropped from the results.
|
/// Gets or sets the record index to start at. All items with a lower index will be dropped from the results.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? StartIndex { get; set; }
|
public int? StartIndex { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the maximum number of records to return.
|
/// Gets or sets the maximum number of records to return.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; set; }
|
public int? Limit { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate.
|
/// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||||
public IReadOnlyList<ItemSortBy> SortBy { get; set; } = Array.Empty<ItemSortBy>();
|
public IReadOnlyList<ItemSortBy>? SortBy { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets sort Order - Ascending,Descending.
|
/// Gets or sets sort order.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||||
public IReadOnlyList<SortOrder> SortOrder { get; set; } = Array.Empty<SortOrder>();
|
public IReadOnlyList<SortOrder>? SortOrder { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the genres to return guide information for.
|
/// Gets or sets the genres to return guide information for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))]
|
[JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))]
|
||||||
public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
|
public IReadOnlyList<string>? Genres { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the genre ids to return guide information for.
|
/// Gets or sets the genre ids to return guide information for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||||
public IReadOnlyList<Guid> GenreIds { get; set; } = Array.Empty<Guid>();
|
public IReadOnlyList<Guid>? GenreIds { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets include image information in output.
|
/// Gets or sets include image information in output.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? EnableImages { get; set; }
|
public bool? EnableImages { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether retrieve total record count.
|
/// Gets or sets a value indicating whether retrieve total record count.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[DefaultValue(true)]
|
||||||
public bool EnableTotalRecordCount { get; set; } = true;
|
public bool EnableTotalRecordCount { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the max number of images to return, per image type.
|
/// Gets or sets the max number of images to return, per image type.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ImageTypeLimit { get; set; }
|
public int? ImageTypeLimit { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the image types to include in the output.
|
/// Gets or sets the image types to include in the output.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||||
public IReadOnlyList<ImageType> EnableImageTypes { get; set; } = Array.Empty<ImageType>();
|
public IReadOnlyList<ImageType>? EnableImageTypes { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets include user data.
|
/// Gets or sets include user data.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool? EnableUserData { get; set; }
|
public bool? EnableUserData { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets filter by series timer id.
|
/// Gets or sets filter by series timer id.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? SeriesTimerId { get; set; }
|
public string? SeriesTimerId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets filter by library series id.
|
/// Gets or sets filter by library series id.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid LibrarySeriesId { get; set; }
|
public Guid? LibrarySeriesId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
|
/// Gets or sets specify additional fields of information to return in the output.
|
||||||
/// Optional.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||||
public IReadOnlyList<ItemFields> Fields { get; set; } = Array.Empty<ItemFields>();
|
public IReadOnlyList<ItemFields>? Fields { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -19,17 +17,11 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private double? _rating;
|
private double? _rating;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the user id.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The user id.</value>
|
|
||||||
public Guid UserId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the key.
|
/// Gets or sets the key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The key.</value>
|
/// <value>The key.</value>
|
||||||
public string Key { get; set; }
|
public required string Key { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the users 0-10 rating.
|
/// Gets or sets the users 0-10 rating.
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.MediaEncoding;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the downmix algorithms capabilities.
|
||||||
|
/// </summary>
|
||||||
|
public static class DownMixAlgorithmsHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The filter string of the DownMixStereoAlgorithms.
|
||||||
|
/// The index is the tuple of (algorithm, layout).
|
||||||
|
/// </summary>
|
||||||
|
public static readonly Dictionary<(DownMixStereoAlgorithms, string), string> AlgorithmFilterStrings = new()
|
||||||
|
{
|
||||||
|
{ (DownMixStereoAlgorithms.Dave750, "5.1"), "pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3" },
|
||||||
|
// Use AC-4 algorithm to downmix 7.1 inputs to 5.1 first
|
||||||
|
{ (DownMixStereoAlgorithms.Dave750, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3" },
|
||||||
|
{ (DownMixStereoAlgorithms.NightmodeDialogue, "5.1"), "pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5" },
|
||||||
|
// Use AC-4 algorithm to downmix 7.1 inputs to 5.1 first
|
||||||
|
{ (DownMixStereoAlgorithms.NightmodeDialogue, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5" },
|
||||||
|
{ (DownMixStereoAlgorithms.Rfc7845, "3.0"), "pan=stereo|c0=0.414214*c2+0.585786*c0|c1=0.414214*c2+0.585786*c1" },
|
||||||
|
{ (DownMixStereoAlgorithms.Rfc7845, "quad"), "pan=stereo|c0=0.422650*c0+0.366025*c2+0.211325*c3|c1=0.422650*c1+0.366025*c3+0.211325*c2" },
|
||||||
|
{ (DownMixStereoAlgorithms.Rfc7845, "5.0"), "pan=stereo|c0=0.460186*c2+0.650802*c0+0.563611*c3+0.325401*c4|c1=0.460186*c2+0.650802*c1+0.563611*c4+0.325401*c3" },
|
||||||
|
{ (DownMixStereoAlgorithms.Rfc7845, "5.1"), "pan=stereo|c0=0.374107*c2+0.529067*c0+0.458186*c4+0.264534*c5+0.374107*c3|c1=0.374107*c2+0.529067*c1+0.458186*c5+0.264534*c4+0.374107*c3" },
|
||||||
|
{ (DownMixStereoAlgorithms.Rfc7845, "6.1"), "pan=stereo|c0=0.321953*c2+0.455310*c0+0.394310*c5+0.227655*c6+0.278819*c4+0.321953*c3|c1=0.321953*c2+0.455310*c1+0.394310*c6+0.227655*c5+0.278819*c4+0.321953*c3" },
|
||||||
|
{ (DownMixStereoAlgorithms.Rfc7845, "7.1"), "pan=stereo|c0=0.274804*c2+0.388631*c0+0.336565*c6+0.194316*c7+0.336565*c4+0.194316*c5+0.274804*c3|c1=0.274804*c2+0.388631*c1+0.336565*c7+0.194316*c6+0.336565*c5+0.194316*c4+0.274804*c3" },
|
||||||
|
{ (DownMixStereoAlgorithms.Ac4, "3.0"), "pan=stereo|c0=c0+0.707*c2|c1=c1+0.707*c2" },
|
||||||
|
{ (DownMixStereoAlgorithms.Ac4, "5.0"), "pan=stereo|c0=c0+0.707*c2+0.707*c3|c1=c1+0.707*c2+0.707*c4" },
|
||||||
|
{ (DownMixStereoAlgorithms.Ac4, "5.1"), "pan=stereo|c0=c0+0.707*c2+0.707*c4|c1=c1+0.707*c2+0.707*c5" },
|
||||||
|
{ (DownMixStereoAlgorithms.Ac4, "7.0"), "pan=5.0(side)|c0=c0|c1=c1|c2=c2|c3=0.707*c3+0.707*c5|c4=0.707*c4+0.707*c6,pan=stereo|c0=c0+0.707*c2+0.707*c3|c1=c1+0.707*c2+0.707*c4" },
|
||||||
|
{ (DownMixStereoAlgorithms.Ac4, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=c0+0.707*c2+0.707*c4|c1=c1+0.707*c2+0.707*c5" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the audio channel layout string from the audio stream
|
||||||
|
/// If the input audio string does not have a valid layout string, guess from channel count.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="audioStream">The audio stream to get layout.</param>
|
||||||
|
/// <returns>Channel Layout string.</returns>
|
||||||
|
public static string InferChannelLayout(MediaStream audioStream)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(audioStream.ChannelLayout))
|
||||||
|
{
|
||||||
|
// Note: BDMVs do not derive this string from ffmpeg, which would cause ambiguity with 4-channel audio
|
||||||
|
// "quad" => 2 front and 2 rear, "4.0" => 3 front and 1 rear
|
||||||
|
// BDMV will always use "4.0" in this case
|
||||||
|
// Because the quad layout is super rare in BDs, we will use "4.0" as is here
|
||||||
|
return audioStream.ChannelLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioStream.Channels is null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we don't have definitive channel layout, we have to guess from the channel count
|
||||||
|
// Guessing is not always correct, but for most videos we don't have to guess like this as the definitive layout is recorded during scan
|
||||||
|
var inferredLayout = audioStream.Channels.Value switch
|
||||||
|
{
|
||||||
|
1 => "mono",
|
||||||
|
2 => "stereo",
|
||||||
|
3 => "2.1", // Could also be 3.0, prefer 2.1
|
||||||
|
4 => "4.0", // Could also be quad (with rear left and rear right) and 3.1 with LFE. prefer 4.0 with front center and back center
|
||||||
|
5 => "5.0",
|
||||||
|
6 => "5.1", // Could also be 6.0 or hexagonal, prefer 5.1
|
||||||
|
7 => "6.1", // Could also be 7.0, prefer 6.1
|
||||||
|
8 => "7.1", // Could also be 8.0, prefer 7.1
|
||||||
|
_ => string.Empty // Return empty string for not supported layout
|
||||||
|
};
|
||||||
|
return inferredLayout;
|
||||||
|
}
|
||||||
|
}
|
@ -64,6 +64,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
|
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
|
||||||
private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
|
private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
|
||||||
private readonly Version _minFFmpegReadrateOption = new Version(5, 0);
|
private readonly Version _minFFmpegReadrateOption = new Version(5, 0);
|
||||||
|
private readonly Version _minFFmpegWorkingVtHwSurface = new Version(7, 0, 1);
|
||||||
private readonly Version _minFFmpegDisplayRotationOption = new Version(6, 0);
|
private readonly Version _minFFmpegDisplayRotationOption = new Version(6, 0);
|
||||||
|
|
||||||
private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
|
private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
|
||||||
@ -2673,29 +2674,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
var filters = new List<string>();
|
var filters = new List<string>();
|
||||||
|
|
||||||
if (channels.HasValue
|
if (channels is 2 && state.AudioStream?.Channels is > 2)
|
||||||
&& channels.Value == 2
|
|
||||||
&& state.AudioStream is not null
|
|
||||||
&& state.AudioStream.Channels.HasValue
|
|
||||||
&& state.AudioStream.Channels.Value == 6)
|
|
||||||
{
|
{
|
||||||
|
var hasDownMixFilter = DownMixAlgorithmsHelper.AlgorithmFilterStrings.TryGetValue((encodingOptions.DownMixStereoAlgorithm, DownMixAlgorithmsHelper.InferChannelLayout(state.AudioStream)), out var downMixFilterString);
|
||||||
|
if (hasDownMixFilter)
|
||||||
|
{
|
||||||
|
filters.Add(downMixFilterString);
|
||||||
|
}
|
||||||
|
|
||||||
if (!encodingOptions.DownMixAudioBoost.Equals(1))
|
if (!encodingOptions.DownMixAudioBoost.Equals(1))
|
||||||
{
|
{
|
||||||
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
|
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (encodingOptions.DownMixStereoAlgorithm)
|
|
||||||
{
|
|
||||||
case DownMixStereoAlgorithms.Dave750:
|
|
||||||
filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3");
|
|
||||||
break;
|
|
||||||
case DownMixStereoAlgorithms.NightmodeDialogue:
|
|
||||||
filters.Add("pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5");
|
|
||||||
break;
|
|
||||||
case DownMixStereoAlgorithms.None:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive;
|
var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive;
|
||||||
@ -5300,6 +5290,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
string vidEncoder)
|
string vidEncoder)
|
||||||
{
|
{
|
||||||
var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
|
var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (!isVtEncoder)
|
if (!isVtEncoder)
|
||||||
{
|
{
|
||||||
@ -5320,6 +5311,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var doDeintH2645 = doDeintH264 || doDeintHevc;
|
var doDeintH2645 = doDeintH264 || doDeintHevc;
|
||||||
var doVtTonemap = IsVideoToolboxTonemapAvailable(state, options);
|
var doVtTonemap = IsVideoToolboxTonemapAvailable(state, options);
|
||||||
var doMetalTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options);
|
var doMetalTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options);
|
||||||
|
var usingHwSurface = isVtDecoder && (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface);
|
||||||
|
|
||||||
var rotation = state.VideoStream?.Rotation ?? 0;
|
var rotation = state.VideoStream?.Rotation ?? 0;
|
||||||
var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
|
var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
|
||||||
@ -5414,23 +5406,25 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
subFilters.Add(subTextSubtitlesFilter);
|
subFilters.Add(subTextSubtitlesFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
subFilters.Add("hwupload=derive_device=videotoolbox");
|
subFilters.Add("hwupload");
|
||||||
overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0");
|
overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (usingHwSurface)
|
||||||
|
{
|
||||||
|
return (mainFilters, subFilters, overlayFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For old jellyfin-ffmpeg that has broken hwsurface, add a hwupload
|
||||||
var needFiltering = mainFilters.Any(f => !string.IsNullOrEmpty(f)) ||
|
var needFiltering = mainFilters.Any(f => !string.IsNullOrEmpty(f)) ||
|
||||||
subFilters.Any(f => !string.IsNullOrEmpty(f)) ||
|
subFilters.Any(f => !string.IsNullOrEmpty(f)) ||
|
||||||
overlayFilters.Any(f => !string.IsNullOrEmpty(f));
|
overlayFilters.Any(f => !string.IsNullOrEmpty(f));
|
||||||
|
|
||||||
// This is a workaround for ffmpeg's hwupload implementation
|
|
||||||
// For VideoToolbox encoders, a hwupload without a valid filter actually consuming its frame
|
|
||||||
// will cause the encoder to produce incorrect frames.
|
|
||||||
if (needFiltering)
|
if (needFiltering)
|
||||||
{
|
{
|
||||||
// INPUT videotoolbox/memory surface(vram/uma)
|
// INPUT videotoolbox/memory surface(vram/uma)
|
||||||
// this will pass-through automatically if in/out format matches.
|
// this will pass-through automatically if in/out format matches.
|
||||||
mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld");
|
mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld");
|
||||||
mainFilters.Insert(0, "hwupload=derive_device=videotoolbox");
|
mainFilters.Insert(0, "hwupload");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (mainFilters, subFilters, overlayFilters);
|
return (mainFilters, subFilters, overlayFilters);
|
||||||
@ -6458,22 +6452,20 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
// VideoToolbox's Hardware surface in ffmpeg is not only slower than hwupload, but also breaks HDR in many cases.
|
// The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment.
|
||||||
// For example: https://trac.ffmpeg.org/ticket/10884
|
bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported();
|
||||||
// Disable it for now.
|
|
||||||
const bool UseHwSurface = false;
|
|
||||||
|
|
||||||
if (is8bitSwFormatsVt)
|
if (is8bitSwFormatsVt)
|
||||||
{
|
{
|
||||||
if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|
if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
|| string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return GetHwaccelType(state, options, "h264", bitDepth, UseHwSurface);
|
return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals("vp8", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals("vp8", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return GetHwaccelType(state, options, "vp8", bitDepth, UseHwSurface);
|
return GetHwaccelType(state, options, "vp8", bitDepth, useHwSurface);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6482,12 +6474,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|
if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
|| string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return GetHwaccelType(state, options, "hevc", bitDepth, UseHwSurface);
|
return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return GetHwaccelType(state, options, "vp9", bitDepth, UseHwSurface);
|
return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7172,7 +7164,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
var channels = state.OutputAudioChannels;
|
var channels = state.OutputAudioChannels;
|
||||||
|
|
||||||
if (channels.HasValue && ((channels.Value != 2 && state.AudioStream?.Channels != 6) || encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None))
|
var useDownMixAlgorithm = state.AudioStream is not null
|
||||||
|
&& DownMixAlgorithmsHelper.AlgorithmFilterStrings.ContainsKey((encodingOptions.DownMixStereoAlgorithm, DownMixAlgorithmsHelper.InferChannelLayout(state.AudioStream)));
|
||||||
|
|
||||||
|
if (channels.HasValue && !useDownMixAlgorithm)
|
||||||
{
|
{
|
||||||
args += " -ac " + channels.Value;
|
args += " -ac " + channels.Value;
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
#pragma warning disable CA1002, CA2227, CS1591
|
#pragma warning disable CA1002, CA2227, CS1591
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
@ -33,8 +31,6 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
set => _remoteImages = value;
|
set => _remoteImages = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<UserItemData> UserDataList { get; set; }
|
|
||||||
|
|
||||||
public List<PersonInfo> People { get; set; }
|
public List<PersonInfo> People { get; set; }
|
||||||
|
|
||||||
public bool HasMetadata { get; set; }
|
public bool HasMetadata { get; set; }
|
||||||
@ -68,32 +64,5 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
People.Clear();
|
People.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserItemData GetOrAddUserData(string userId)
|
|
||||||
{
|
|
||||||
UserDataList ??= new List<UserItemData>();
|
|
||||||
|
|
||||||
UserItemData userData = null;
|
|
||||||
|
|
||||||
foreach (var i in UserDataList)
|
|
||||||
{
|
|
||||||
if (string.Equals(userId, i.UserId.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
userData = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userData is null)
|
|
||||||
{
|
|
||||||
userData = new UserItemData()
|
|
||||||
{
|
|
||||||
UserId = new Guid(userId)
|
|
||||||
};
|
|
||||||
|
|
||||||
UserDataList.Add(userData);
|
|
||||||
}
|
|
||||||
|
|
||||||
return userData;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -365,10 +365,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
|||||||
break;
|
break;
|
||||||
case "CollectionNumber":
|
case "CollectionNumber":
|
||||||
var tmdbCollection = reader.ReadNormalizedString();
|
var tmdbCollection = reader.ReadNormalizedString();
|
||||||
if (!string.IsNullOrEmpty(tmdbCollection))
|
item.TrySetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -502,10 +499,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
|||||||
if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
|
if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
|
||||||
{
|
{
|
||||||
var id = reader.ReadElementContentAsString();
|
var id = reader.ReadElementContentAsString();
|
||||||
if (!string.IsNullOrWhiteSpace(id))
|
item.TrySetProviderId(providerIdValue, id);
|
||||||
{
|
|
||||||
item.SetProviderId(providerIdValue, id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -1325,38 +1325,23 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
// These support multiple values, but for now we only store the first.
|
// These support multiple values, but for now we only store the first.
|
||||||
var mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Artist Id"))
|
var mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Artist Id"))
|
||||||
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMARTISTID"));
|
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMARTISTID"));
|
||||||
if (!string.IsNullOrEmpty(mb))
|
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb);
|
||||||
{
|
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb);
|
|
||||||
}
|
|
||||||
|
|
||||||
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Artist Id"))
|
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Artist Id"))
|
||||||
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ARTISTID"));
|
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ARTISTID"));
|
||||||
if (!string.IsNullOrEmpty(mb))
|
audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, mb);
|
||||||
{
|
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzArtist, mb);
|
|
||||||
}
|
|
||||||
|
|
||||||
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Id"))
|
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Id"))
|
||||||
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMID"));
|
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMID"));
|
||||||
if (!string.IsNullOrEmpty(mb))
|
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, mb);
|
||||||
{
|
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, mb);
|
|
||||||
}
|
|
||||||
|
|
||||||
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Group Id"))
|
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Group Id"))
|
||||||
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASEGROUPID"));
|
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASEGROUPID"));
|
||||||
if (!string.IsNullOrEmpty(mb))
|
audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb);
|
||||||
{
|
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb);
|
|
||||||
}
|
|
||||||
|
|
||||||
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Track Id"))
|
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Track Id"))
|
||||||
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASETRACKID"));
|
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASETRACKID"));
|
||||||
if (!string.IsNullOrEmpty(mb))
|
audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, mb);
|
||||||
{
|
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzTrack, mb);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetMultipleMusicBrainzId(string value)
|
private string GetMultipleMusicBrainzId(string value)
|
||||||
|
@ -908,7 +908,18 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec));
|
var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)).FirstOrDefault();
|
||||||
|
|
||||||
|
var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
|
||||||
|
|
||||||
|
var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null;
|
||||||
|
|
||||||
|
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
|
||||||
|
{
|
||||||
|
playlistItem.TranscodeReasons |= TranscodeReason.AudioChannelsNotSupported;
|
||||||
|
playlistItem.TargetAudioStream.Channels = playlistItem.TranscodingMaxAudioChannels;
|
||||||
|
}
|
||||||
|
|
||||||
playlistItem.AudioCodecs = audioCodecs;
|
playlistItem.AudioCodecs = audioCodecs;
|
||||||
if (directAudioStream is not null)
|
if (directAudioStream is not null)
|
||||||
{
|
{
|
||||||
@ -971,7 +982,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Honor requested max channels
|
// Honor requested max channels
|
||||||
playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
|
playlistItem.GlobalMaxAudioChannels = channelsExceedsLimit ? playlistItem.TranscodingMaxAudioChannels : options.MaxAudioChannels;
|
||||||
|
|
||||||
int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
|
int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
|
||||||
playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
|
playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
|
||||||
@ -1293,7 +1304,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
// Check audio codec
|
// Check audio codec
|
||||||
MediaStream? selectedAudioStream = null;
|
MediaStream? selectedAudioStream = null;
|
||||||
if (candidateAudioStreams.Any())
|
if (candidateAudioStreams.Count != 0)
|
||||||
{
|
{
|
||||||
selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
|
selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
|
||||||
if (selectedAudioStream is null)
|
if (selectedAudioStream is null)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Dto
|
namespace MediaBrowser.Model.Dto
|
||||||
@ -66,12 +65,12 @@ namespace MediaBrowser.Model.Dto
|
|||||||
/// Gets or sets the key.
|
/// Gets or sets the key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The key.</value>
|
/// <value>The key.</value>
|
||||||
public string Key { get; set; }
|
public required string Key { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the item identifier.
|
/// Gets or sets the item identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The item identifier.</value>
|
/// <value>The item identifier.</value>
|
||||||
public string ItemId { get; set; }
|
public Guid ItemId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
namespace MediaBrowser.Model.Entities;
|
namespace MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An enum representing an algorithm to downmix 6ch+ to stereo.
|
/// An enum representing an algorithm to downmix surround sound to stereo.
|
||||||
/// Algorithms sourced from https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg/1410620#1410620.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum DownMixStereoAlgorithms
|
public enum DownMixStereoAlgorithms
|
||||||
{
|
{
|
||||||
@ -13,11 +12,24 @@ public enum DownMixStereoAlgorithms
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Algorithm by Dave_750.
|
/// Algorithm by Dave_750.
|
||||||
|
/// Sourced from https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg/1410620#1410620.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Dave750 = 1,
|
Dave750 = 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Nightmode Dialogue algorithm.
|
/// Nightmode Dialogue algorithm.
|
||||||
|
/// Sourced from https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg/1410620#1410620.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
NightmodeDialogue = 2
|
NightmodeDialogue = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RFC7845 Section 5.1.1.5 defined algorithm.
|
||||||
|
/// </summary>
|
||||||
|
Rfc7845 = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AC-4 standard algorithm with its default gain values.
|
||||||
|
/// Defined in ETSI TS 103 190 Section 6.2.17.
|
||||||
|
/// </summary>
|
||||||
|
Ac4 = 4
|
||||||
}
|
}
|
||||||
|
@ -202,7 +202,7 @@ namespace MediaBrowser.Model.Entities
|
|||||||
|| dvProfile == 8
|
|| dvProfile == 8
|
||||||
|| dvProfile == 9))
|
|| dvProfile == 9))
|
||||||
{
|
{
|
||||||
var title = "DV Profile " + dvProfile;
|
var title = "Dolby Vision Profile " + dvProfile;
|
||||||
|
|
||||||
if (dvBlCompatId > 0)
|
if (dvBlCompatId > 0)
|
||||||
{
|
{
|
||||||
@ -214,6 +214,7 @@ namespace MediaBrowser.Model.Entities
|
|||||||
1 => title + " (HDR10)",
|
1 => title + " (HDR10)",
|
||||||
2 => title + " (SDR)",
|
2 => title + " (SDR)",
|
||||||
4 => title + " (HLG)",
|
4 => title + " (HLG)",
|
||||||
|
6 => title + " (HDR10)", // Technically means Blu-ray, but practically always HDR10
|
||||||
_ => title
|
_ => title
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -336,7 +337,11 @@ namespace MediaBrowser.Model.Entities
|
|||||||
attributes.Add(Codec.ToUpperInvariant());
|
attributes.Add(Codec.ToUpperInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VideoRange != VideoRange.Unknown)
|
if (VideoDoViTitle is not null)
|
||||||
|
{
|
||||||
|
attributes.Add(VideoDoViTitle);
|
||||||
|
}
|
||||||
|
else if (VideoRange != VideoRange.Unknown)
|
||||||
{
|
{
|
||||||
attributes.Add(VideoRange.ToString());
|
attributes.Add(VideoRange.ToString());
|
||||||
}
|
}
|
||||||
|
@ -3,177 +3,214 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Entities
|
namespace MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class ProviderIdsExtensions.
|
||||||
|
/// </summary>
|
||||||
|
public static class ProviderIdsExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class ProviderIdsExtensions.
|
/// Case insensitive dictionary of <see cref="MetadataProvider"/> string representation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ProviderIdsExtensions
|
private static readonly Dictionary<string, string> _metadataProviderEnumDictionary =
|
||||||
|
Enum.GetValues<MetadataProvider>()
|
||||||
|
.ToDictionary(
|
||||||
|
enumValue => enumValue.ToString(),
|
||||||
|
enumValue => enumValue.ToString(),
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if this instance has an id for the given provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance.</param>
|
||||||
|
/// <param name="name">The of the provider name.</param>
|
||||||
|
/// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
|
||||||
|
public static bool HasProviderId(this IHasProviderIds instance, string name)
|
||||||
|
=> instance.TryGetProviderId(name, out _);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if this instance has an id for the given provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance.</param>
|
||||||
|
/// <param name="provider">The provider.</param>
|
||||||
|
/// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
|
||||||
|
public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider)
|
||||||
|
=> instance.HasProviderId(provider.ToString());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a provider id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance.</param>
|
||||||
|
/// <param name="name">The name.</param>
|
||||||
|
/// <param name="id">The provider id.</param>
|
||||||
|
/// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
|
||||||
|
public static bool TryGetProviderId(this IHasProviderIds instance, string name, [NotNullWhen(true)] out string? id)
|
||||||
{
|
{
|
||||||
/// <summary>
|
ArgumentNullException.ThrowIfNull(instance);
|
||||||
/// Case insensitive dictionary of <see cref="MetadataProvider"/> string representation.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly Dictionary<string, string> _metadataProviderEnumDictionary =
|
|
||||||
Enum.GetValues<MetadataProvider>()
|
|
||||||
.ToDictionary(
|
|
||||||
enumValue => enumValue.ToString(),
|
|
||||||
enumValue => enumValue.ToString(),
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
/// <summary>
|
if (instance.ProviderIds is null)
|
||||||
/// Checks if this instance has an id for the given provider.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="instance">The instance.</param>
|
|
||||||
/// <param name="name">The of the provider name.</param>
|
|
||||||
/// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
|
|
||||||
public static bool HasProviderId(this IHasProviderIds instance, string name)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(instance);
|
id = null;
|
||||||
|
return false;
|
||||||
return instance.TryGetProviderId(name, out _);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var foundProviderId = instance.ProviderIds.TryGetValue(name, out id);
|
||||||
/// Checks if this instance has an id for the given provider.
|
// This occurs when searching with Identify (and possibly in other places)
|
||||||
/// </summary>
|
if (string.IsNullOrEmpty(id))
|
||||||
/// <param name="instance">The instance.</param>
|
|
||||||
/// <param name="provider">The provider.</param>
|
|
||||||
/// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
|
|
||||||
public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider)
|
|
||||||
{
|
{
|
||||||
return instance.HasProviderId(provider.ToString());
|
id = null;
|
||||||
|
foundProviderId = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return foundProviderId;
|
||||||
/// Gets a provider id.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="instance">The instance.</param>
|
/// <summary>
|
||||||
/// <param name="name">The name.</param>
|
/// Gets a provider id.
|
||||||
/// <param name="id">The provider id.</param>
|
/// </summary>
|
||||||
/// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
|
/// <param name="instance">The instance.</param>
|
||||||
public static bool TryGetProviderId(this IHasProviderIds instance, string name, [NotNullWhen(true)] out string? id)
|
/// <param name="provider">The provider.</param>
|
||||||
|
/// <param name="id">The provider id.</param>
|
||||||
|
/// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
|
||||||
|
public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [NotNullWhen(true)] out string? id)
|
||||||
|
{
|
||||||
|
return instance.TryGetProviderId(provider.ToString(), out id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a provider id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance.</param>
|
||||||
|
/// <param name="name">The name.</param>
|
||||||
|
/// <returns>System.String.</returns>
|
||||||
|
public static string? GetProviderId(this IHasProviderIds instance, string name)
|
||||||
|
{
|
||||||
|
instance.TryGetProviderId(name, out string? id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a provider id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance.</param>
|
||||||
|
/// <param name="provider">The provider.</param>
|
||||||
|
/// <returns>System.String.</returns>
|
||||||
|
public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider)
|
||||||
|
{
|
||||||
|
return instance.GetProviderId(provider.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a provider id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance.</param>
|
||||||
|
/// <param name="name">The name, this should not contain a '=' character.</param>
|
||||||
|
/// <param name="value">The value.</param>
|
||||||
|
/// <remarks>Due to how deserialization from the database works the name can not contain '='.</remarks>
|
||||||
|
/// <returns><c>true</c> if the provider id got set successfully; otherwise, <c>false</c>.</returns>
|
||||||
|
public static bool TrySetProviderId(this IHasProviderIds instance, string? name, string? value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(instance);
|
||||||
|
|
||||||
|
// When name contains a '=' it can't be deserialized from the database
|
||||||
|
if (string.IsNullOrWhiteSpace(name)
|
||||||
|
|| string.IsNullOrWhiteSpace(value)
|
||||||
|
|| name.Contains('=', StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(instance);
|
return false;
|
||||||
|
|
||||||
if (instance.ProviderIds is null)
|
|
||||||
{
|
|
||||||
id = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var foundProviderId = instance.ProviderIds.TryGetValue(name, out id);
|
|
||||||
// This occurs when searching with Identify (and possibly in other places)
|
|
||||||
if (string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
id = null;
|
|
||||||
foundProviderId = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundProviderId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// Ensure it exists
|
||||||
/// Gets a provider id.
|
instance.ProviderIds ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
/// </summary>
|
|
||||||
/// <param name="instance">The instance.</param>
|
// Match on internal MetadataProvider enum string values before adding arbitrary providers
|
||||||
/// <param name="provider">The provider.</param>
|
if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue))
|
||||||
/// <param name="id">The provider id.</param>
|
|
||||||
/// <returns><c>true</c> if a provider id with the given name was found; otherwise <c>false</c>.</returns>
|
|
||||||
public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [NotNullWhen(true)] out string? id)
|
|
||||||
{
|
{
|
||||||
return instance.TryGetProviderId(provider.ToString(), out id);
|
instance.ProviderIds[enumValue] = value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
instance.ProviderIds[name] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return true;
|
||||||
/// Gets a provider id.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="instance">The instance.</param>
|
/// <summary>
|
||||||
/// <param name="name">The name.</param>
|
/// Sets a provider id.
|
||||||
/// <returns>System.String.</returns>
|
/// </summary>
|
||||||
public static string? GetProviderId(this IHasProviderIds instance, string name)
|
/// <param name="instance">The instance.</param>
|
||||||
|
/// <param name="provider">The provider.</param>
|
||||||
|
/// <param name="value">The value.</param>
|
||||||
|
/// <returns><c>true</c> if the provider id got set successfully; otherwise, <c>false</c>.</returns>
|
||||||
|
public static bool TrySetProviderId(this IHasProviderIds instance, MetadataProvider provider, string? value)
|
||||||
|
=> instance.TrySetProviderId(provider.ToString(), value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a provider id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance.</param>
|
||||||
|
/// <param name="name">The name, this should not contain a '=' character.</param>
|
||||||
|
/// <param name="value">The value.</param>
|
||||||
|
/// <remarks>Due to how deserialization from the database works the name can not contain '='.</remarks>
|
||||||
|
public static void SetProviderId(this IHasProviderIds instance, string name, string value)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(instance);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(value);
|
||||||
|
|
||||||
|
// When name contains a '=' it can't be deserialized from the database
|
||||||
|
if (name.Contains('=', StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
instance.TryGetProviderId(name, out string? id);
|
throw new ArgumentException("Provider id name cannot contain '='", nameof(name));
|
||||||
return id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// Ensure it exists
|
||||||
/// Gets a provider id.
|
instance.ProviderIds ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
/// </summary>
|
|
||||||
/// <param name="instance">The instance.</param>
|
// Match on internal MetadataProvider enum string values before adding arbitrary providers
|
||||||
/// <param name="provider">The provider.</param>
|
if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue))
|
||||||
/// <returns>System.String.</returns>
|
|
||||||
public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider)
|
|
||||||
{
|
{
|
||||||
return instance.GetProviderId(provider.ToString());
|
instance.ProviderIds[enumValue] = value;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
/// <summary>
|
|
||||||
/// Sets a provider id.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="instance">The instance.</param>
|
|
||||||
/// <param name="name">The name, this should not contain a '=' character.</param>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
/// <remarks>Due to how deserialization from the database works the name can not contain '='.</remarks>
|
|
||||||
public static void SetProviderId(this IHasProviderIds instance, string name, string value)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(instance);
|
instance.ProviderIds[name] = value;
|
||||||
ArgumentException.ThrowIfNullOrEmpty(name);
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(value);
|
|
||||||
|
|
||||||
// When name contains a '=' it can't be deserialized from the database
|
|
||||||
if (name.Contains('=', StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Provider id name cannot contain '='", nameof(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure it exists
|
|
||||||
instance.ProviderIds ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// Match on internal MetadataProvider enum string values before adding arbitrary providers
|
|
||||||
if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue))
|
|
||||||
{
|
|
||||||
instance.ProviderIds[enumValue] = value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
instance.ProviderIds[name] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets a provider id.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="instance">The instance.</param>
|
|
||||||
/// <param name="provider">The provider.</param>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
public static void SetProviderId(this IHasProviderIds instance, MetadataProvider provider, string value)
|
|
||||||
{
|
|
||||||
instance.SetProviderId(provider.ToString(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a provider id.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="instance">The instance.</param>
|
|
||||||
/// <param name="name">The name.</param>
|
|
||||||
public static void RemoveProviderId(this IHasProviderIds instance, string name)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(instance);
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(name);
|
|
||||||
|
|
||||||
instance.ProviderIds?.Remove(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a provider id.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="instance">The instance.</param>
|
|
||||||
/// <param name="provider">The provider.</param>
|
|
||||||
public static void RemoveProviderId(this IHasProviderIds instance, MetadataProvider provider)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(instance);
|
|
||||||
|
|
||||||
instance.ProviderIds?.Remove(provider.ToString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a provider id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance.</param>
|
||||||
|
/// <param name="provider">The provider.</param>
|
||||||
|
/// <param name="value">The value.</param>
|
||||||
|
public static void SetProviderId(this IHasProviderIds instance, MetadataProvider provider, string value)
|
||||||
|
=> instance.SetProviderId(provider.ToString(), value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a provider id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance.</param>
|
||||||
|
/// <param name="name">The name.</param>
|
||||||
|
public static void RemoveProviderId(this IHasProviderIds instance, string name)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(instance);
|
||||||
|
ArgumentException.ThrowIfNullOrEmpty(name);
|
||||||
|
|
||||||
|
instance.ProviderIds?.Remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a provider id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance.</param>
|
||||||
|
/// <param name="provider">The provider.</param>
|
||||||
|
public static void RemoveProviderId(this IHasProviderIds instance, MetadataProvider provider)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(instance);
|
||||||
|
|
||||||
|
instance.ProviderIds?.Remove(provider.ToString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,60 @@
|
|||||||
#nullable disable
|
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Querying
|
namespace MediaBrowser.Model.Querying;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query result container.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of item contained in the query result.</typeparam>
|
||||||
|
public class QueryResult<T>
|
||||||
{
|
{
|
||||||
public class QueryResult<T>
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="QueryResult{T}" /> class.
|
||||||
|
/// </summary>
|
||||||
|
public QueryResult()
|
||||||
{
|
{
|
||||||
public QueryResult()
|
Items = Array.Empty<T>();
|
||||||
{
|
|
||||||
Items = Array.Empty<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryResult(IReadOnlyList<T> items)
|
|
||||||
{
|
|
||||||
Items = items;
|
|
||||||
TotalRecordCount = items.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public QueryResult(int? startIndex, int? totalRecordCount, IReadOnlyList<T> items)
|
|
||||||
{
|
|
||||||
StartIndex = startIndex ?? 0;
|
|
||||||
TotalRecordCount = totalRecordCount ?? items.Count;
|
|
||||||
Items = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the items.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The items.</value>
|
|
||||||
public IReadOnlyList<T> Items { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the total number of records available.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The total record count.</value>
|
|
||||||
public int TotalRecordCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the index of the first record in Items.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>First record index.</value>
|
|
||||||
public int StartIndex { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="QueryResult{T}" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">The list of items.</param>
|
||||||
|
public QueryResult(IReadOnlyList<T> items)
|
||||||
|
{
|
||||||
|
Items = items;
|
||||||
|
TotalRecordCount = items.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="QueryResult{T}" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startIndex">The start index that was used to build the item list.</param>
|
||||||
|
/// <param name="totalRecordCount">The total count of items.</param>
|
||||||
|
/// <param name="items">The list of items.</param>
|
||||||
|
public QueryResult(int? startIndex, int? totalRecordCount, IReadOnlyList<T> items)
|
||||||
|
{
|
||||||
|
StartIndex = startIndex ?? 0;
|
||||||
|
TotalRecordCount = totalRecordCount ?? items.Count;
|
||||||
|
Items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the items.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The items.</value>
|
||||||
|
public IReadOnlyList<T> Items { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the total number of records available.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The total record count.</value>
|
||||||
|
public int TotalRecordCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the index of the first record in Items.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>First record index.</value>
|
||||||
|
public int StartIndex { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#nullable disable
|
using System;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Session
|
namespace MediaBrowser.Model.Session
|
||||||
@ -12,12 +12,12 @@ namespace MediaBrowser.Model.Session
|
|||||||
/// Gets or sets the user id.
|
/// Gets or sets the user id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The user id.</value>
|
/// <value>The user id.</value>
|
||||||
public string UserId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the user data list.
|
/// Gets or sets the user data list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The user data list.</value>
|
/// <value>The user data list.</value>
|
||||||
public UserItemDataDto[] UserDataList { get; set; }
|
public required UserItemDataDto[] UserDataList { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||||
<PackageReference Include="Newtonsoft.Json" />
|
<PackageReference Include="Newtonsoft.Json" />
|
||||||
<PackageReference Include="PlaylistsNET" />
|
<PackageReference Include="PlaylistsNET" />
|
||||||
<PackageReference Include="TagLibSharp" />
|
<PackageReference Include="z440.atl.core"/>
|
||||||
<PackageReference Include="TMDbLib" />
|
<PackageReference Include="TMDbLib" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using ATL;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@ -18,7 +19,6 @@ using MediaBrowser.Model.Dto;
|
|||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TagLib;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.MediaInfo
|
namespace MediaBrowser.Providers.MediaInfo
|
||||||
{
|
{
|
||||||
@ -27,6 +27,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class AudioFileProber
|
public class AudioFileProber
|
||||||
{
|
{
|
||||||
|
private const char InternalValueSeparator = '\u001F';
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly IItemRepository _itemRepo;
|
private readonly IItemRepository _itemRepo;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
@ -61,6 +62,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
_lyricResolver = lyricResolver;
|
_lyricResolver = lyricResolver;
|
||||||
_lyricManager = lyricManager;
|
_lyricManager = lyricManager;
|
||||||
|
ATL.Settings.DisplayValueSeparator = InternalValueSeparator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -127,7 +129,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
|
|
||||||
audio.RunTimeTicks = mediaInfo.RunTimeTicks;
|
audio.RunTimeTicks = mediaInfo.RunTimeTicks;
|
||||||
audio.Size = mediaInfo.Size;
|
audio.Size = mediaInfo.Size;
|
||||||
audio.PremiereDate = mediaInfo.PremiereDate;
|
|
||||||
|
|
||||||
// Add external lyrics first to prevent the lrc file get overwritten on first scan
|
// Add external lyrics first to prevent the lrc file get overwritten on first scan
|
||||||
var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams);
|
var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams);
|
||||||
@ -157,60 +158,23 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
/// <param name="tryExtractEmbeddedLyrics">Whether to extract embedded lyrics to lrc file. </param>
|
/// <param name="tryExtractEmbeddedLyrics">Whether to extract embedded lyrics to lrc file. </param>
|
||||||
private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics)
|
private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics)
|
||||||
{
|
{
|
||||||
Tag? tags = null;
|
Track track = new Track(audio.Path);
|
||||||
try
|
|
||||||
{
|
|
||||||
using var file = TagLib.File.Create(audio.Path);
|
|
||||||
var tagTypes = file.TagTypesOnDisk;
|
|
||||||
|
|
||||||
if (tagTypes.HasFlag(TagTypes.Id3v2))
|
// ATL will fall back to filename as title when it does not understand the metadata
|
||||||
{
|
if (track.MetadataFormats.All(mf => mf.Equals(ATL.Factory.UNKNOWN_FORMAT)))
|
||||||
tags = file.GetTag(TagTypes.Id3v2);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.Ape))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.Ape);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.FlacMetadata))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.FlacMetadata);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.Apple))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.Apple);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.Xiph))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.Xiph);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.AudibleMetadata))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.AudibleMetadata);
|
|
||||||
}
|
|
||||||
else if (tagTypes.HasFlag(TagTypes.Id3v1))
|
|
||||||
{
|
|
||||||
tags = file.GetTag(TagTypes.Id3v1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning(e, "TagLib-Sharp does not support this audio");
|
track.Title = mediaInfo.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
tags ??= new TagLib.Id3v2.Tag();
|
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
|
||||||
tags.AlbumArtists ??= mediaInfo.AlbumArtists;
|
track.Year ??= mediaInfo.ProductionYear;
|
||||||
tags.Album ??= mediaInfo.Album;
|
track.TrackNumber ??= mediaInfo.IndexNumber;
|
||||||
tags.Title ??= mediaInfo.Name;
|
track.DiscNumber ??= mediaInfo.ParentIndexNumber;
|
||||||
tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
|
|
||||||
tags.Performers ??= mediaInfo.Artists;
|
|
||||||
tags.Genres ??= mediaInfo.Genres;
|
|
||||||
tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track;
|
|
||||||
tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc;
|
|
||||||
|
|
||||||
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
||||||
{
|
{
|
||||||
var people = new List<PersonInfo>();
|
var people = new List<PersonInfo>();
|
||||||
var albumArtists = tags.AlbumArtists;
|
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator);
|
||||||
foreach (var albumArtist in albumArtists)
|
foreach (var albumArtist in albumArtists)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(albumArtist))
|
if (!string.IsNullOrEmpty(albumArtist))
|
||||||
@ -223,7 +187,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var performers = tags.Performers;
|
var performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator);
|
||||||
foreach (var performer in performers)
|
foreach (var performer in performers)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(performer))
|
if (!string.IsNullOrEmpty(performer))
|
||||||
@ -236,7 +200,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var composer in tags.Composers)
|
foreach (var composer in track.Composer.Split(InternalValueSeparator))
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(composer))
|
if (!string.IsNullOrEmpty(composer))
|
||||||
{
|
{
|
||||||
@ -277,27 +241,32 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title))
|
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title))
|
||||||
{
|
{
|
||||||
audio.Name = tags.Title;
|
audio.Name = track.Title;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata)
|
if (options.ReplaceAllMetadata)
|
||||||
{
|
{
|
||||||
audio.Album = tags.Album;
|
audio.Album = track.Album;
|
||||||
audio.IndexNumber = Convert.ToInt32(tags.Track);
|
audio.IndexNumber = track.TrackNumber;
|
||||||
audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
|
audio.ParentIndexNumber = track.DiscNumber;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
audio.Album ??= tags.Album;
|
audio.Album ??= track.Album;
|
||||||
audio.IndexNumber ??= Convert.ToInt32(tags.Track);
|
audio.IndexNumber ??= track.TrackNumber;
|
||||||
audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc);
|
audio.ParentIndexNumber ??= track.DiscNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags.Year != 0)
|
if (track.Date.HasValue)
|
||||||
{
|
{
|
||||||
var year = Convert.ToInt32(tags.Year);
|
audio.PremiereDate = track.Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track.Year.HasValue)
|
||||||
|
{
|
||||||
|
var year = track.Year.Value;
|
||||||
audio.ProductionYear = year;
|
audio.ProductionYear = year;
|
||||||
|
|
||||||
if (!audio.PremiereDate.HasValue)
|
if (!audio.PremiereDate.HasValue)
|
||||||
@ -308,64 +277,91 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
catch (ArgumentOutOfRangeException ex)
|
catch (ArgumentOutOfRangeException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, tags.Year);
|
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audio.LockedFields.Contains(MetadataField.Genres))
|
if (!audio.LockedFields.Contains(MetadataField.Genres))
|
||||||
{
|
{
|
||||||
|
var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||||
audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0
|
audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0
|
||||||
? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
|
? genres
|
||||||
: audio.Genres;
|
: audio.Genres;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!double.IsNaN(tags.ReplayGainTrackGain))
|
track.AdditionalFields.TryGetValue("REPLAYGAIN_TRACK_GAIN", out var trackGainTag);
|
||||||
|
|
||||||
|
if (trackGainTag is not null)
|
||||||
{
|
{
|
||||||
audio.NormalizationGain = (float)tags.ReplayGainTrackGain;
|
if (trackGainTag.EndsWith("db", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
trackGainTag = trackGainTag[..^2].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (float.TryParse(trackGainTag, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
|
||||||
|
{
|
||||||
|
audio.NormalizationGain = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
|
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
|
||||||
&& !string.IsNullOrEmpty(tags.MusicBrainzArtistId))
|
|
||||||
{
|
{
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId);
|
if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ARTISTID", out var musicBrainzArtistTag)
|
||||||
|
|| track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
|
||||||
|
&& !string.IsNullOrEmpty(musicBrainzArtistTag))
|
||||||
|
{
|
||||||
|
audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
|
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
|
||||||
&& !string.IsNullOrEmpty(tags.MusicBrainzReleaseArtistId))
|
|
||||||
{
|
{
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId);
|
if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMARTISTID", out var musicBrainzReleaseArtistIdTag)
|
||||||
|
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag))
|
||||||
|
&& !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag))
|
||||||
|
{
|
||||||
|
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
|
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
|
||||||
&& !string.IsNullOrEmpty(tags.MusicBrainzReleaseId))
|
|
||||||
{
|
{
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId);
|
if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMID", out var musicBrainzReleaseIdTag)
|
||||||
|
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag))
|
||||||
|
&& !string.IsNullOrEmpty(musicBrainzReleaseIdTag))
|
||||||
|
{
|
||||||
|
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
|
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
|
||||||
&& !string.IsNullOrEmpty(tags.MusicBrainzReleaseGroupId))
|
|
||||||
{
|
{
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId);
|
if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASEGROUPID", out var musicBrainzReleaseGroupIdTag)
|
||||||
|
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag))
|
||||||
|
&& !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag))
|
||||||
|
{
|
||||||
|
audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
|
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
|
||||||
{
|
{
|
||||||
// Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`.
|
if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASETRACKID", out var trackMbId)
|
||||||
// See https://github.com/mono/taglib-sharp/issues/304
|
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId))
|
||||||
var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack);
|
&& !string.IsNullOrEmpty(trackMbId))
|
||||||
if (trackMbId is not null)
|
|
||||||
{
|
{
|
||||||
audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
|
audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save extracted lyrics if they exist,
|
// Save extracted lyrics if they exist,
|
||||||
// and if the audio doesn't yet have lyrics.
|
// and if the audio doesn't yet have lyrics.
|
||||||
if (!string.IsNullOrWhiteSpace(tags.Lyrics)
|
var lyrics = track.Lyrics.SynchronizedLyrics.Count > 0 ? track.Lyrics.FormatSynchToLRC() : track.Lyrics.UnsynchronizedLyrics;
|
||||||
|
if (!string.IsNullOrWhiteSpace(lyrics)
|
||||||
&& tryExtractEmbeddedLyrics)
|
&& tryExtractEmbeddedLyrics)
|
||||||
{
|
{
|
||||||
await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false);
|
await _lyricManager.SaveLyricAsync(audio, "lrc", lyrics).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,10 +220,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
|
|||||||
item.HomePageUrl = result.Website;
|
item.HomePageUrl = result.Website;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(result.imdbID))
|
item.TrySetProviderId(MetadataProvider.Imdb, result.imdbID);
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
|
|
||||||
}
|
|
||||||
|
|
||||||
ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
|
ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
|
||||||
|
|
||||||
|
@ -81,11 +81,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
|||||||
}
|
}
|
||||||
|
|
||||||
remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
|
remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
|
||||||
|
remoteResult.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId);
|
||||||
if (!string.IsNullOrWhiteSpace(movie.ImdbId))
|
|
||||||
{
|
|
||||||
remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new[] { remoteResult };
|
return new[] { remoteResult };
|
||||||
}
|
}
|
||||||
|
@ -56,10 +56,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture));
|
result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture));
|
||||||
if (!string.IsNullOrEmpty(personResult.ExternalIds.ImdbId))
|
result.TrySetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
|
||||||
{
|
|
||||||
result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new[] { result };
|
return new[] { result };
|
||||||
}
|
}
|
||||||
@ -129,11 +126,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
|
|||||||
}
|
}
|
||||||
|
|
||||||
item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
|
item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
|
||||||
|
item.TrySetProviderId(MetadataProvider.Imdb, person.ImdbId);
|
||||||
if (!string.IsNullOrEmpty(person.ImdbId))
|
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Imdb, person.ImdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.HasMetadata = true;
|
result.HasMetadata = true;
|
||||||
result.Item = item;
|
result.Item = item;
|
||||||
|
@ -187,20 +187,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
};
|
};
|
||||||
|
|
||||||
var externalIds = episodeResult.ExternalIds;
|
var externalIds = episodeResult.ExternalIds;
|
||||||
if (!string.IsNullOrEmpty(externalIds?.TvdbId))
|
item.TrySetProviderId(MetadataProvider.Tvdb, externalIds?.TvdbId);
|
||||||
{
|
item.TrySetProviderId(MetadataProvider.Imdb, externalIds?.ImdbId);
|
||||||
item.SetProviderId(MetadataProvider.Tvdb, externalIds.TvdbId);
|
item.TrySetProviderId(MetadataProvider.TvRage, externalIds?.TvrageId);
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(externalIds?.ImdbId))
|
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Imdb, externalIds.ImdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(externalIds?.TvrageId))
|
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.TvRage, externalIds.TvrageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (episodeResult.Videos?.Results is not null)
|
if (episodeResult.Videos?.Results is not null)
|
||||||
{
|
{
|
||||||
|
@ -73,10 +73,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
result.Item.Name = seasonResult.Name;
|
result.Item.Name = seasonResult.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId))
|
result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId);
|
||||||
{
|
|
||||||
result.Item.SetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO why was this disabled?
|
// TODO why was this disabled?
|
||||||
var credits = seasonResult.Credits;
|
var credits = seasonResult.Credits;
|
||||||
|
@ -135,15 +135,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
|
remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
|
||||||
if (series.ExternalIds is not null)
|
if (series.ExternalIds is not null)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(series.ExternalIds.ImdbId))
|
remoteResult.TrySetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId);
|
||||||
{
|
|
||||||
remoteResult.SetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(series.ExternalIds.TvdbId))
|
remoteResult.TrySetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId);
|
||||||
{
|
|
||||||
remoteResult.SetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
|
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
|
||||||
@ -289,20 +283,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
|
|||||||
var ids = seriesResult.ExternalIds;
|
var ids = seriesResult.ExternalIds;
|
||||||
if (ids is not null)
|
if (ids is not null)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(ids.ImdbId))
|
series.TrySetProviderId(MetadataProvider.Imdb, ids.ImdbId);
|
||||||
{
|
series.TrySetProviderId(MetadataProvider.TvRage, ids.TvrageId);
|
||||||
series.SetProviderId(MetadataProvider.Imdb, ids.ImdbId);
|
series.TrySetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ids.TvrageId))
|
|
||||||
{
|
|
||||||
series.SetProviderId(MetadataProvider.TvRage, ids.TvrageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ids.TvdbId))
|
|
||||||
{
|
|
||||||
series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentRatings = seriesResult.ContentRatings.Results ?? new List<ContentRating>();
|
var contentRatings = seriesResult.ContentRatings.Results ?? new List<ContentRating>();
|
||||||
|
@ -572,10 +572,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
|
|
||||||
var provider = reader.GetAttribute("type");
|
var provider = reader.GetAttribute("type");
|
||||||
var providerId = reader.ReadElementContentAsString();
|
var providerId = reader.ReadElementContentAsString();
|
||||||
if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(providerId))
|
item.TrySetProviderId(provider, providerId);
|
||||||
{
|
|
||||||
item.SetProviderId(provider, providerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "thumb":
|
case "thumb":
|
||||||
@ -604,10 +601,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
if (_validProviderIds.TryGetValue(readerName, out string? providerIdValue))
|
if (_validProviderIds.TryGetValue(readerName, out string? providerIdValue))
|
||||||
{
|
{
|
||||||
var id = reader.ReadElementContentAsString();
|
var id = reader.ReadElementContentAsString();
|
||||||
if (!string.IsNullOrWhiteSpace(providerIdValue) && !string.IsNullOrWhiteSpace(id))
|
item.TrySetProviderId(providerIdValue, id);
|
||||||
{
|
|
||||||
item.SetProviderId(providerIdValue, id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -59,80 +59,50 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Extract episode details from the first episodedetails block
|
// Extract episode details from the first episodedetails block
|
||||||
using (var stringReader = new StringReader(xml))
|
ReadEpisodeDetailsFromXml(item, xml, settings, cancellationToken);
|
||||||
using (var reader = XmlReader.Create(stringReader, settings))
|
|
||||||
{
|
|
||||||
reader.MoveToContent();
|
|
||||||
reader.Read();
|
|
||||||
|
|
||||||
// Loop through each element
|
|
||||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (reader.NodeType == XmlNodeType.Element)
|
|
||||||
{
|
|
||||||
FetchDataFromXmlNode(reader, item);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
reader.Read();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the last episode number from nfo
|
// Extract the last episode number from nfo
|
||||||
// Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode
|
// Retrieves all additional episodedetails blocks from the rest of the nfo and concatenates the name, originalTitle and overview tags with the first episode
|
||||||
// This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
|
// This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
|
||||||
var name = new StringBuilder(item.Item.Name);
|
var name = new StringBuilder(item.Item.Name);
|
||||||
|
var originalTitle = new StringBuilder(item.Item.OriginalTitle);
|
||||||
var overview = new StringBuilder(item.Item.Overview);
|
var overview = new StringBuilder(item.Item.Overview);
|
||||||
while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
|
while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
|
||||||
{
|
{
|
||||||
xml = xmlFile.Substring(0, index + srch.Length);
|
xml = xmlFile.Substring(0, index + srch.Length);
|
||||||
xmlFile = xmlFile.Substring(index + srch.Length);
|
xmlFile = xmlFile.Substring(index + srch.Length);
|
||||||
|
|
||||||
using (var stringReader = new StringReader(xml))
|
var additionalEpisode = new MetadataResult<Episode>()
|
||||||
using (var reader = XmlReader.Create(stringReader, settings))
|
|
||||||
{
|
{
|
||||||
reader.MoveToContent();
|
Item = new Episode()
|
||||||
|
};
|
||||||
|
|
||||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
// Extract episode details from additional episodedetails block
|
||||||
{
|
ReadEpisodeDetailsFromXml(additionalEpisode, xml, settings, cancellationToken);
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (reader.NodeType == XmlNodeType.Element)
|
if (!string.IsNullOrEmpty(additionalEpisode.Item.Name))
|
||||||
{
|
{
|
||||||
switch (reader.Name)
|
name.Append(" / ").Append(additionalEpisode.Item.Name);
|
||||||
{
|
}
|
||||||
case "name":
|
|
||||||
case "title":
|
|
||||||
case "localtitle":
|
|
||||||
name.Append(" / ").Append(reader.ReadElementContentAsString());
|
|
||||||
break;
|
|
||||||
case "episode":
|
|
||||||
{
|
|
||||||
if (int.TryParse(reader.ReadElementContentAsString(), out var num))
|
|
||||||
{
|
|
||||||
item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
if (!string.IsNullOrEmpty(additionalEpisode.Item.Overview))
|
||||||
}
|
{
|
||||||
|
overview.Append(" / ").Append(additionalEpisode.Item.Overview);
|
||||||
|
}
|
||||||
|
|
||||||
case "biography":
|
if (!string.IsNullOrEmpty(additionalEpisode.Item.OriginalTitle))
|
||||||
case "plot":
|
{
|
||||||
case "review":
|
originalTitle.Append(" / ").Append(additionalEpisode.Item.OriginalTitle);
|
||||||
overview.Append(" / ").Append(reader.ReadElementContentAsString());
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.Read();
|
if (additionalEpisode.Item.IndexNumber != null)
|
||||||
}
|
{
|
||||||
|
item.Item.IndexNumberEnd = Math.Max((int)additionalEpisode.Item.IndexNumber, item.Item.IndexNumberEnd ?? (int)additionalEpisode.Item.IndexNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item.Item.Name = name.ToString();
|
item.Item.Name = name.ToString();
|
||||||
|
item.Item.OriginalTitle = originalTitle.ToString();
|
||||||
item.Item.Overview = overview.ToString();
|
item.Item.Overview = overview.ToString();
|
||||||
}
|
}
|
||||||
catch (XmlException)
|
catch (XmlException)
|
||||||
@ -200,5 +170,33 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the episode details from the given xml and saves the result in the provided result item.
|
||||||
|
/// </summary>
|
||||||
|
private void ReadEpisodeDetailsFromXml(MetadataResult<Episode> item, string xml, XmlReaderSettings settings, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using (var stringReader = new StringReader(xml))
|
||||||
|
using (var reader = XmlReader.Create(stringReader, settings))
|
||||||
|
{
|
||||||
|
reader.MoveToContent();
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
// Loop through each element
|
||||||
|
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (reader.NodeType == XmlNodeType.Element)
|
||||||
|
{
|
||||||
|
FetchDataFromXmlNode(reader, item);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reader.Read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,15 +65,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
tmdbId = contentId;
|
tmdbId = contentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(imdbId))
|
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
||||||
{
|
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||||
item.SetProviderId(MetadataProvider.Imdb, imdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(tmdbId))
|
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Tmdb, tmdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -83,10 +76,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
var movie = item as Movie;
|
var movie = item as Movie;
|
||||||
|
|
||||||
var tmdbcolid = reader.GetAttribute("tmdbcolid");
|
var tmdbcolid = reader.GetAttribute("tmdbcolid");
|
||||||
if (!string.IsNullOrWhiteSpace(tmdbcolid) && movie is not null)
|
movie?.TrySetProviderId(MetadataProvider.TmdbCollection, tmdbcolid);
|
||||||
{
|
|
||||||
movie.SetProviderId(MetadataProvider.TmdbCollection, tmdbcolid);
|
|
||||||
}
|
|
||||||
|
|
||||||
var val = reader.ReadInnerXml();
|
var val = reader.ReadInnerXml();
|
||||||
|
|
||||||
|
@ -48,29 +48,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
{
|
{
|
||||||
case "id":
|
case "id":
|
||||||
{
|
{
|
||||||
string? imdbId = reader.GetAttribute("IMDB");
|
item.TrySetProviderId(MetadataProvider.Imdb, reader.GetAttribute("IMDB"));
|
||||||
string? tmdbId = reader.GetAttribute("TMDB");
|
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
|
||||||
string? tvdbId = reader.GetAttribute("TVDB");
|
|
||||||
|
|
||||||
|
string? tvdbId = reader.GetAttribute("TVDB");
|
||||||
if (string.IsNullOrWhiteSpace(tvdbId))
|
if (string.IsNullOrWhiteSpace(tvdbId))
|
||||||
{
|
{
|
||||||
tvdbId = reader.ReadElementContentAsString();
|
tvdbId = reader.ReadElementContentAsString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(imdbId))
|
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Imdb, imdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(tmdbId))
|
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Tmdb, tmdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(tvdbId))
|
|
||||||
{
|
|
||||||
item.SetProviderId(MetadataProvider.Tvdb, tvdbId);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -54,11 +54,6 @@ namespace MediaBrowser.XbmcMetadata.Providers
|
|||||||
result.People = tmpItem.People;
|
result.People = tmpItem.People;
|
||||||
result.Images = tmpItem.Images;
|
result.Images = tmpItem.Images;
|
||||||
result.RemoteImages = tmpItem.RemoteImages;
|
result.RemoteImages = tmpItem.RemoteImages;
|
||||||
|
|
||||||
if (tmpItem.UserDataList is not null)
|
|
||||||
{
|
|
||||||
result.UserDataList = tmpItem.UserDataList;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -105,6 +105,28 @@
|
|||||||
<Rule Id="CA1851" Action="Error" />
|
<Rule Id="CA1851" Action="Error" />
|
||||||
<!-- error on CA1854: Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup -->
|
<!-- error on CA1854: Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup -->
|
||||||
<Rule Id="CA1854" Action="Error" />
|
<Rule Id="CA1854" Action="Error" />
|
||||||
|
<!-- error on CA1860: Avoid using 'Enumerable.Any()' extension method -->
|
||||||
|
<Rule Id="CA1860" Action="Error" />
|
||||||
|
<!-- error on CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons -->
|
||||||
|
<Rule Id="CA1862" Action="Error" />
|
||||||
|
<!-- error on CA1863: Use 'CompositeFormat' -->
|
||||||
|
<Rule Id="CA1863" Action="Error" />
|
||||||
|
<!-- error on CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method -->
|
||||||
|
<Rule Id="CA1864" Action="Error" />
|
||||||
|
<!-- error on CA1865-CA1867: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char -->
|
||||||
|
<Rule Id="CA1865" Action="Error" />
|
||||||
|
<Rule Id="CA1866" Action="Error" />
|
||||||
|
<Rule Id="CA1867" Action="Error" />
|
||||||
|
<!-- error on CA1868: Unnecessary call to 'Contains' for sets -->
|
||||||
|
<Rule Id="CA1868" Action="Error" />
|
||||||
|
<!-- error on CA1869: Cache and reuse 'JsonSerializerOptions' instances -->
|
||||||
|
<Rule Id="CA1869" Action="Error" />
|
||||||
|
<!-- error on CA1870: Use a cached 'SearchValues' instance -->
|
||||||
|
<Rule Id="CA1870" Action="Error" />
|
||||||
|
<!-- error on CA1871: Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull' -->
|
||||||
|
<Rule Id="CA1871" Action="Error" />
|
||||||
|
<!-- error on CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString' -->
|
||||||
|
<Rule Id="CA1872" Action="Error" />
|
||||||
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
|
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
|
||||||
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
|
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
|
||||||
<Rule Id="CA2016" Action="Error" />
|
<Rule Id="CA2016" Action="Error" />
|
||||||
|
@ -479,10 +479,7 @@ public class GuideManager : IGuideManager
|
|||||||
DateModified = DateTime.UtcNow
|
DateModified = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(info.Etag))
|
item.TrySetProviderId(EtagKey, info.Etag);
|
||||||
{
|
|
||||||
item.SetProviderId(EtagKey, info.Etag);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
@ -51,8 +51,8 @@ namespace Jellyfin.Model.Tests
|
|||||||
[InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
[InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
||||||
[InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
[InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
||||||
[InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
|
[InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
|
||||||
[InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
|
[InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
|
||||||
[InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
|
[InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
|
||||||
// AndroidPixel
|
// AndroidPixel
|
||||||
[InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
[InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
||||||
[InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
[InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
||||||
@ -205,8 +205,8 @@ namespace Jellyfin.Model.Tests
|
|||||||
[InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
[InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
||||||
[InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
[InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
||||||
[InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
|
[InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
|
||||||
[InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
|
[InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
|
||||||
[InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
|
[InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
|
||||||
// AndroidPixel
|
// AndroidPixel
|
||||||
[InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
[InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
||||||
[InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
[InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
||||||
@ -432,7 +432,14 @@ namespace Jellyfin.Model.Tests
|
|||||||
if (targetAudioStream?.IsExternal == false)
|
if (targetAudioStream?.IsExternal == false)
|
||||||
{
|
{
|
||||||
// Check expected audio codecs (1)
|
// Check expected audio codecs (1)
|
||||||
Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
|
if ((why & TranscodeReason.AudioChannelsNotSupported) == 0)
|
||||||
|
{
|
||||||
|
Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Equal(targetAudioStream.Channels, streamInfo.TranscodingMaxAudioChannels);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (transcodeMode.Equals("Remux", StringComparison.Ordinal))
|
else if (transcodeMode.Equals("Remux", StringComparison.Ordinal))
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
using Emby.Naming.TV;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Jellyfin.Naming.Tests.TV
|
|
||||||
{
|
|
||||||
public class SeasonFolderTests
|
|
||||||
{
|
|
||||||
[Theory]
|
|
||||||
[InlineData("/Drive/Season 1", 1, true)]
|
|
||||||
[InlineData("/Drive/Season 2", 2, true)]
|
|
||||||
[InlineData("/Drive/Season 02", 2, true)]
|
|
||||||
[InlineData("/Drive/Seinfeld/S02", 2, true)]
|
|
||||||
[InlineData("/Drive/Seinfeld/2", 2, true)]
|
|
||||||
[InlineData("/Drive/Season 2009", 2009, true)]
|
|
||||||
[InlineData("/Drive/Season1", 1, true)]
|
|
||||||
[InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
|
|
||||||
[InlineData("/Drive/Season 7 (2016)", 7, false)]
|
|
||||||
[InlineData("/Drive/Staffel 7 (2016)", 7, false)]
|
|
||||||
[InlineData("/Drive/Stagione 7 (2016)", 7, false)]
|
|
||||||
[InlineData("/Drive/Season (8)", null, false)]
|
|
||||||
[InlineData("/Drive/3.Staffel", 3, false)]
|
|
||||||
[InlineData("/Drive/s06e05", null, false)]
|
|
||||||
[InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
|
|
||||||
[InlineData("/Drive/extras", 0, true)]
|
|
||||||
[InlineData("/Drive/specials", 0, true)]
|
|
||||||
public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory)
|
|
||||||
{
|
|
||||||
var result = SeasonPathParser.Parse(path, true, true);
|
|
||||||
|
|
||||||
Assert.Equal(result.SeasonNumber is not null, result.Success);
|
|
||||||
Assert.Equal(result.SeasonNumber, seasonNumber);
|
|
||||||
Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
37
tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
Normal file
37
tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Emby.Naming.TV;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Jellyfin.Naming.Tests.TV;
|
||||||
|
|
||||||
|
public class SeasonPathParserTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/Drive/Season 1", 1, true)]
|
||||||
|
[InlineData("/Drive/s1", 1, true)]
|
||||||
|
[InlineData("/Drive/S1", 1, true)]
|
||||||
|
[InlineData("/Drive/Season 2", 2, true)]
|
||||||
|
[InlineData("/Drive/Season 02", 2, true)]
|
||||||
|
[InlineData("/Drive/Seinfeld/S02", 2, true)]
|
||||||
|
[InlineData("/Drive/Seinfeld/2", 2, true)]
|
||||||
|
[InlineData("/Drive/Seinfeld - S02", 2, true)]
|
||||||
|
[InlineData("/Drive/Season 2009", 2009, true)]
|
||||||
|
[InlineData("/Drive/Season1", 1, true)]
|
||||||
|
[InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
|
||||||
|
[InlineData("/Drive/Season 7 (2016)", 7, false)]
|
||||||
|
[InlineData("/Drive/Staffel 7 (2016)", 7, false)]
|
||||||
|
[InlineData("/Drive/Stagione 7 (2016)", 7, false)]
|
||||||
|
[InlineData("/Drive/Season (8)", null, false)]
|
||||||
|
[InlineData("/Drive/3.Staffel", 3, false)]
|
||||||
|
[InlineData("/Drive/s06e05", null, false)]
|
||||||
|
[InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
|
||||||
|
[InlineData("/Drive/extras", 0, true)]
|
||||||
|
[InlineData("/Drive/specials", 0, true)]
|
||||||
|
public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory)
|
||||||
|
{
|
||||||
|
var result = SeasonPathParser.Parse(path, true, true);
|
||||||
|
|
||||||
|
Assert.Equal(result.SeasonNumber is not null, result.Success);
|
||||||
|
Assert.Equal(result.SeasonNumber, seasonNumber);
|
||||||
|
Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
|
||||||
|
}
|
||||||
|
}
|
@ -123,6 +123,30 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
|
|||||||
Assert.Equal(2004, item.ProductionYear);
|
Assert.Equal(2004, item.ProductionYear);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fetch_Valid_MultiEpisode_With_Missing_Tags_Success()
|
||||||
|
{
|
||||||
|
var result = new MetadataResult<Episode>()
|
||||||
|
{
|
||||||
|
Item = new Episode()
|
||||||
|
};
|
||||||
|
|
||||||
|
_parser.Fetch(result, "Test Data/Stargate Atlantis S01E01-E04.nfo", CancellationToken.None);
|
||||||
|
|
||||||
|
var item = result.Item;
|
||||||
|
// <title> provided for episode 1, 3 and 4
|
||||||
|
Assert.Equal("Rising / Hide and Seek / Thirty-Eight Minutes", item.Name);
|
||||||
|
// <originaltitle> provided for all episodes
|
||||||
|
Assert.Equal("Rising (1) / Rising (2) / Hide and Seek / Thirty-Eight Minutes", item.OriginalTitle);
|
||||||
|
Assert.Equal(1, item.IndexNumber);
|
||||||
|
Assert.Equal(4, item.IndexNumberEnd);
|
||||||
|
Assert.Equal(1, item.ParentIndexNumber);
|
||||||
|
// <plot> only provided for episode 1
|
||||||
|
Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview);
|
||||||
|
Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
|
||||||
|
Assert.Equal(2004, item.ProductionYear);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_GivenFileWithThumbWithoutAspect_Success()
|
public void Parse_GivenFileWithThumbWithoutAspect_Success()
|
||||||
{
|
{
|
||||||
|
@ -53,7 +53,10 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
|
|||||||
|
|
||||||
var userData = new Mock<IUserDataManager>();
|
var userData = new Mock<IUserDataManager>();
|
||||||
userData.Setup(x => x.GetUserData(_testUser, It.IsAny<BaseItem>()))
|
userData.Setup(x => x.GetUserData(_testUser, It.IsAny<BaseItem>()))
|
||||||
.Returns(new UserItemData());
|
.Returns(new UserItemData()
|
||||||
|
{
|
||||||
|
Key = "Something"
|
||||||
|
});
|
||||||
|
|
||||||
var directoryService = new Mock<IDirectoryService>();
|
var directoryService = new Mock<IDirectoryService>();
|
||||||
_localImageFileMetadata = new FileSystemMetadata()
|
_localImageFileMetadata = new FileSystemMetadata()
|
||||||
|
@ -7,6 +7,18 @@
|
|||||||
<thumb>https://artworks.thetvdb.com/banners/episodes/70851/25333.jpg</thumb>
|
<thumb>https://artworks.thetvdb.com/banners/episodes/70851/25333.jpg</thumb>
|
||||||
<watched>false</watched>
|
<watched>false</watched>
|
||||||
<rating>8.0</rating>
|
<rating>8.0</rating>
|
||||||
|
<actor>
|
||||||
|
<name>Joe Flanigan</name>
|
||||||
|
<role>John Sheppard</role>
|
||||||
|
<order>0</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg</thumb>
|
||||||
|
</actor>
|
||||||
|
<actor>
|
||||||
|
<name>David Hewlett</name>
|
||||||
|
<role>Rodney McKay</role>
|
||||||
|
<order>1</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg</thumb>
|
||||||
|
</actor>
|
||||||
</episodedetails>
|
</episodedetails>
|
||||||
<episodedetails>
|
<episodedetails>
|
||||||
<title>Rising (2)</title>
|
<title>Rising (2)</title>
|
||||||
@ -17,4 +29,16 @@
|
|||||||
<thumb>https://artworks.thetvdb.com/banners/episodes/70851/25334.jpg</thumb>
|
<thumb>https://artworks.thetvdb.com/banners/episodes/70851/25334.jpg</thumb>
|
||||||
<watched>false</watched>
|
<watched>false</watched>
|
||||||
<rating>7.9</rating>
|
<rating>7.9</rating>
|
||||||
|
<actor>
|
||||||
|
<name>Joe Flanigan</name>
|
||||||
|
<role>John Sheppard</role>
|
||||||
|
<order>0</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg</thumb>
|
||||||
|
</actor>
|
||||||
|
<actor>
|
||||||
|
<name>David Hewlett</name>
|
||||||
|
<role>Rodney McKay</role>
|
||||||
|
<order>1</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg</thumb>
|
||||||
|
</actor>
|
||||||
</episodedetails>
|
</episodedetails>
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
<episodedetails>
|
||||||
|
<title>Rising</title>
|
||||||
|
<originaltitle>Rising (1)</originaltitle>
|
||||||
|
<season>1</season>
|
||||||
|
<episode>1</episode>
|
||||||
|
<aired>2004-07-16</aired>
|
||||||
|
<plot>A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.</plot>
|
||||||
|
<thumb>https://artworks.thetvdb.com/banners/episodes/70851/25333.jpg</thumb>
|
||||||
|
<watched>false</watched>
|
||||||
|
<rating>8.0</rating>
|
||||||
|
<actor>
|
||||||
|
<name>Joe Flanigan</name>
|
||||||
|
<role>John Sheppard</role>
|
||||||
|
<order>0</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg</thumb>
|
||||||
|
</actor>
|
||||||
|
<actor>
|
||||||
|
<name>David Hewlett</name>
|
||||||
|
<role>Rodney McKay</role>
|
||||||
|
<order>1</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg</thumb>
|
||||||
|
</actor>
|
||||||
|
</episodedetails>
|
||||||
|
<episodedetails>
|
||||||
|
<originaltitle>Rising (2)</originaltitle>
|
||||||
|
<season>1</season>
|
||||||
|
<episode>2</episode>
|
||||||
|
<aired>2004-07-16</aired>
|
||||||
|
<thumb>https://artworks.thetvdb.com/banners/episodes/70851/25334.jpg</thumb>
|
||||||
|
<watched>false</watched>
|
||||||
|
<rating>7.9</rating>
|
||||||
|
<actor>
|
||||||
|
<name>Joe Flanigan</name>
|
||||||
|
<role>John Sheppard</role>
|
||||||
|
<order>0</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg</thumb>
|
||||||
|
</actor>
|
||||||
|
<actor>
|
||||||
|
<name>David Hewlett</name>
|
||||||
|
<role>Rodney McKay</role>
|
||||||
|
<order>1</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg</thumb>
|
||||||
|
</actor>
|
||||||
|
</episodedetails>
|
||||||
|
<episodedetails>
|
||||||
|
<title>Hide and Seek</title>
|
||||||
|
<originaltitle>Hide and Seek</originaltitle>
|
||||||
|
<season>1</season>
|
||||||
|
<episode>3</episode>
|
||||||
|
<aired>2004-07-23</aired>
|
||||||
|
<thumb>https://artworks.thetvdb.com/banners/episodes/70851/25335.jpg</thumb>
|
||||||
|
<watched>false</watched>
|
||||||
|
<rating>7.5</rating>
|
||||||
|
<actor>
|
||||||
|
<name>Joe Flanigan</name>
|
||||||
|
<role>John Sheppard</role>
|
||||||
|
<order>0</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg</thumb>
|
||||||
|
</actor>
|
||||||
|
<actor>
|
||||||
|
<name>David Hewlett</name>
|
||||||
|
<role>Rodney McKay</role>
|
||||||
|
<order>1</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg</thumb>
|
||||||
|
</actor>
|
||||||
|
</episodedetails>
|
||||||
|
<episodedetails>
|
||||||
|
<title>Thirty-Eight Minutes</title>
|
||||||
|
<originaltitle>Thirty-Eight Minutes</originaltitle>
|
||||||
|
<season>1</season>
|
||||||
|
<episode>4</episode>
|
||||||
|
<aired>2004-07-23</aired>
|
||||||
|
<thumb>https://artworks.thetvdb.com/banners/episodes/70851/25336.jpg</thumb>
|
||||||
|
<watched>false</watched>
|
||||||
|
<rating>7.5</rating>
|
||||||
|
<actor>
|
||||||
|
<name>Joe Flanigan</name>
|
||||||
|
<role>John Sheppard</role>
|
||||||
|
<order>0</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg</thumb>
|
||||||
|
</actor>
|
||||||
|
<actor>
|
||||||
|
<name>David Hewlett</name>
|
||||||
|
<role>Rodney McKay</role>
|
||||||
|
<order>1</order>
|
||||||
|
<thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg</thumb>
|
||||||
|
</actor>
|
||||||
|
</episodedetails>
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user