Compare commits

...

40 Commits

Author SHA1 Message Date
Joshua M. Boniface
3dda25412c Bump version to 10.7.1 2021-03-21 19:18:08 -04:00
Joshua M. Boniface
0183ef8e89 Merge pull request from GHSA-wg4c-c9g9-rxhx
Fix issues 1 through 5 from GHSL-2021-050

(cherry picked from commit fe8cf29cad)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:13:08 -04:00
Claus Vium
75f39f0f2a Merge pull request #5559 from cvium/fix-tmdb-search-clean
Clean the entity name for non-words before searching

(cherry picked from commit 9360fecb31)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
966217e6a9 Merge pull request #5550 from cvium/revert_underscore_multiversion
(cherry picked from commit f42cee4790)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Joshua M. Boniface
328bcadabf Merge pull request #5532 from cvium/fix_episode_extras_questionmark
(cherry picked from commit 890a490776)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
0f38b2ffb2 Merge pull request #5518 from crobibero/missing-endpoints
Add missing InstantMix endpoints

(cherry picked from commit 8bb2420a25)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
40f4780825 Merge pull request #5515 from jellyfin/fix-refresh-endpoint
fix refresh endpoint

(cherry picked from commit 260b48ef9d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Claus Vium
546ffbe4f7 Merge pull request #5512 from crobibero/api-spec-version
(cherry picked from commit 94820f569b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Claus Vium
d00218c370 Merge pull request #5510 from BaronGreenback/DlnaFirstFix
Fix: Streaming crashing due to no deviceProfileId match.
(cherry picked from commit 109f24514f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
679d3f5873 Merge pull request #5504 from crobibero/json-string-converter
(cherry picked from commit 1a0ce16f4d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
787ad44323 Merge pull request #5500 from crobibero/api-integration-fix
Fix third party integration

(cherry picked from commit 7a988ef77d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
2ce6b347f5 Merge pull request #5480 from crobibero/api-session-message-type
Add SessionMessageType to generated openapi spec

(cherry picked from commit e3adc9ab74)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bill Thornton
318c1f7f0c Merge pull request #5476 from jellyfin/EraYaN-nuget-ci
Remove BuildPackage dependency for PublishNuget in CI

(cherry picked from commit 9fe3ca7a92)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Claus Vium
ed15cb1571 Merge pull request #5475 from BaronGreenback/SSDPFix
(cherry picked from commit baa43c6b41)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:10:13 -04:00
Bond-009
c171bac71a Merge pull request #5461 from cvium/fix_multiversion
(cherry picked from commit d967267cef)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:09:59 -04:00
Bond-009
be5f511fc7 Merge pull request #5457 from cvium/fix_double_artist
(cherry picked from commit a037e30b41)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:27 -04:00
Claus Vium
a65c97c8f7 Merge pull request #5447 from joshuaboniface/fix-fedora-build
Remove Microsoft repo from install step

(cherry picked from commit 88a8fa7100)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:27 -04:00
Claus Vium
3fbe10364b Merge pull request #5444 from Ullmie02/hdhr-fix
(cherry picked from commit 329edd9dbe)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:27 -04:00
Claus Vium
88ab008112 Merge pull request #5431 from cvium/fix_tmdb_imdbid
Use imdbid as fallback in movie provider

(cherry picked from commit 84e16a8535)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:27 -04:00
Bond-009
1518f6d325 Merge pull request #5428 from cvium/fix_tmdb_year
Default to the searchinfo year, fallback to parsed year

(cherry picked from commit 97fd136a8c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:26 -04:00
Claus Vium
53576fe1b8 Merge pull request #5403 from BaronGreenback/DLNAProfileFix
(cherry picked from commit 5592967497)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:26 -04:00
Claus Vium
da3b7bb684 Merge pull request #5324 from danieladov/master
Fix duplicated movies when group movies into collections is enabled

(cherry picked from commit bd70f56218)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-21 19:08:26 -04:00
Joshua M. Boniface
4a320b26b5 Bump version to 10.7.0
Also clear out old changelogs since these don't actually make sense
anyways.
2021-03-08 18:11:29 -05:00
Joshua M. Boniface
63868eca40 Merge pull request #5409 from ikomhoog/master
(cherry picked from commit 82d88bdec6)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:26 -05:00
Claus Vium
f6e8493d69 Merge pull request #5407 from Bond-009/hack
(cherry picked from commit 90cdd1345d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:26 -05:00
Joshua M. Boniface
3c3b536e81 Merge pull request #5406 from cvium/trycleanstring-dont-die-on-me
(cherry picked from commit 0ef8bea125)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:26 -05:00
Joshua M. Boniface
a10eea41ac Merge pull request #5402 from Ullmie02/fix-null-size
Use FileShare.None when creating files

(cherry picked from commit 480dd66428)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:25 -05:00
Bond-009
42d0c1ac5f Merge pull request #5381 from cvium/fix-network-substitution
(cherry picked from commit 497ea57fd2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:25 -05:00
Joshua M. Boniface
b01290013e Merge pull request #5315 from BaronGreenback/FixFor5280Part2
(cherry picked from commit 3c46f10e3d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-08 18:08:20 -05:00
Bond-009
132335a747 Merge pull request #5383 from cvium/fix-mergeversions-overflow
do not pick a linked item as primary when merging versions

(cherry picked from commit 3741be51ec)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:30:11 -05:00
Claus Vium
75d3d120d3 Fix UpdateMediaPath model binding (#5378)
(cherry picked from commit d0a2d00b29)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:28:08 -05:00
Bond-009
e8890cc682 Merge pull request #5377 from cvium/fix-tmdb-image-languages
Do not use language or imagelanguages when searching for images with TMDb

(cherry picked from commit 1d87274cc2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:27:58 -05:00
Bond-009
e4bf57c739 Merge pull request #5375 from crobibero/default-api-value
Specify defaults or set query parameter to nullable

(cherry picked from commit a0f6bc14a2)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:27:58 -05:00
Claus Vium
046dd7fa60 Merge pull request #5356 from cvium/fix_provideridextensions
return false when providerid is null or empty

(cherry picked from commit ddc62a89ba)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:27:58 -05:00
David Ullmer
5e18ab3604 Fix TMDb search name containing year (#5349)
(cherry picked from commit 8f99bdd07c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:26:54 -05:00
dkanada
7545b1286b Merge pull request #5345 from BaronGreenback/IP6Fix
Dual IP4 / IP6 server fails on non-windows platforms

(cherry picked from commit 8615847a8a)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:26:41 -05:00
Bond-009
b99db64f8f Merge pull request #5342 from BaronGreenback/errorMessageCorrection
Corrected logging message

(cherry picked from commit 1f0bbe266c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:26:41 -05:00
Claus Vium
20810eedbe Merge pull request #5339 from Bond-009/hasproviderids
Revert breaking change to HasProviderId

(cherry picked from commit e858e5f0b8)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:26:41 -05:00
BaronGreenback
2d88b8346d Remove Content-Length header from DLNA HEAD request (#5335)
(cherry picked from commit d819a1d928)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:26:38 -05:00
Bond-009
eafaccae5d Merge pull request #5004 from jellyfin/camera-upload
remove unused notification type

(cherry picked from commit bffebce909)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2021-03-06 14:24:50 -05:00
98 changed files with 1222 additions and 611 deletions

View File

@@ -160,7 +160,6 @@ jobs:
dependsOn:
- BuildPackage
- BuildDocker
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
pool:
vmImage: 'ubuntu-latest'
@@ -186,9 +185,6 @@ jobs:
- job: PublishNuget
displayName: 'Publish NuGet packages'
dependsOn:
- BuildPackage
condition: succeeded('BuildPackage')
pool:
vmImage: 'ubuntu-latest'

View File

@@ -49,6 +49,7 @@
- [h1nk](https://github.com/h1nk)
- [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)

View File

@@ -333,7 +333,12 @@ namespace Emby.Dlna
throw new ArgumentNullException(nameof(id));
}
var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
if (info == null)
{
return null;
}
return ParseProfileFile(info.Path, info.Info.Type);
}
@@ -395,7 +400,8 @@ namespace Emby.Dlna
{
Directory.CreateDirectory(systemProfilesPath);
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}

View File

@@ -128,7 +128,8 @@ namespace Emby.Dlna.Main
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
if (_disabled)
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{
_logger.LogError("The DLNA specification does not support HTTPS.");
}
@@ -316,7 +317,7 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
if (_appHost.PublishedServerUrl == null)
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
{
// DLNA will only work over http, so we must reset to http:// : {port}.
uri.Scheme = "http";

View File

@@ -132,7 +132,7 @@ namespace Emby.Dlna.PlayTo
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
{
if (_disposed)
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
return;
}

View File

@@ -1,5 +1,7 @@
#pragma warning disable CS1591
using System;
using System.Globalization;
using System.Linq;
using MediaBrowser.Model.Dlna;
@@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
{
public DefaultProfile()
{
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
Name = "Generic Device";
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";

View File

@@ -69,7 +69,7 @@ namespace Emby.Dlna.Ssdp
{
lock (_syncLock)
{
if (_listenerCount > 0 && _deviceLocator == null)
if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
{
_deviceLocator = new SsdpDeviceLocator(_commsServer);

View File

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

View File

@@ -33,6 +33,12 @@ namespace Emby.Naming.Video
private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
{
if (string.IsNullOrEmpty(name))
{
newName = ReadOnlySpan<char>.Empty;
return false;
}
var match = expression.Match(name);
int index = match.Index;
if (match.Success && index != 0)
@@ -41,7 +47,7 @@ namespace Emby.Naming.Video
return true;
}
newName = string.Empty;
newName = ReadOnlySpan<char>.Empty;
return false;
}
}

View File

@@ -222,20 +222,21 @@ namespace Emby.Naming.Video
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.ToString();
}
// Remove the folder name before cleaning as we don't care about cleaning that part
if (folderName.Length <= testFilename.Length)
{
testFilename = testFilename.Substring(folderName.Length).Trim();
}
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.Trim().ToString();
}
// The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(testFilename)
|| testFilename[0].Equals('-')
|| testFilename[0].Equals('_')
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
|| testFilename[0] == '-'
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
}
return false;

View File

@@ -75,10 +75,6 @@ namespace Emby.Notifications
Type = NotificationType.VideoPlaybackStopped.ToString()
},
new NotificationTypeInfo
{
Type = NotificationType.CameraImageUploaded.ToString()
},
new NotificationTypeInfo
{
Type = NotificationType.UserLockedOut.ToString()
},
@@ -114,10 +110,6 @@ namespace Emby.Notifications
{
note.Category = _localization.GetLocalizedString("Plugin");
}
else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1)
{
note.Category = _localization.GetLocalizedString("Sync");
}
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
{
note.Category = _localization.GetLocalizedString("User");

View File

@@ -53,7 +53,8 @@ namespace Emby.Server.Implementations.AppBase
Directory.CreateDirectory(directory);
// Save it after load in case we got new items
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
fs.Write(newBytes, 0, newBytesLen);
}

View File

@@ -43,6 +43,7 @@ using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Udp;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.Networking.Configuration;
@@ -98,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
@@ -117,6 +119,7 @@ namespace Emby.Server.Implementations
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
@@ -134,9 +137,6 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; }
/// <inheritdoc />
public Uri PublishedServerUrl => _startupOptions.PublishedServerUrl;
public virtual bool CanLaunchWebBrowser
{
get
@@ -230,6 +230,11 @@ namespace Emby.Server.Implementations
/// </summary>
public int HttpsPort { get; private set; }
/// <summary>
/// Gets the value of the PublishedServerUrl setting.
/// </summary>
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
/// <summary>
/// Gets the server configuration manager.
/// </summary>
@@ -242,12 +247,14 @@ namespace Emby.Server.Implementations
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startupConfig,
IFileSystem fileSystem,
IServiceCollection serviceCollection)
{
@@ -271,6 +278,7 @@ namespace Emby.Server.Implementations
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_startupOptions = options;
_startupConfig = startupConfig;
// Initialize runtime stat collection
if (ServerConfigurationManager.Configuration.EnableMetrics)
@@ -1151,10 +1159,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
{
// Published server ends with a /
if (_startupOptions.PublishedServerUrl != null)
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(ipAddress, out port);
@@ -1171,10 +1179,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(HttpRequest request, int? port = null)
{
// Published server ends with a /
if (_startupOptions.PublishedServerUrl != null)
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(request, out port);
@@ -1191,10 +1199,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(string hostname, int? port = null)
{
// Published server ends with a /
if (_startupOptions.PublishedServerUrl != null)
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(hostname, out port);

View File

@@ -344,7 +344,20 @@ namespace Emby.Server.Implementations.Collections
}
else
{
results[item.Id] = item;
var alreadyInResults = false;
foreach (var child in item.GetMediaSources(true))
{
if (Guid.TryParse(child.Id, out var id) && results.ContainsKey(id))
{
alreadyInResults = true;
break;
}
}
if (!alreadyInResults)
{
results[item.Id] = item;
}
}
}
}

View File

@@ -1,5 +1,5 @@
#pragma warning disable CS1591
#nullable enable
using System;
namespace Emby.Server.Implementations
@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the value of the --ffmpeg command line option.
/// </summary>
string FFmpegPath { get; }
string? FFmpegPath { get; }
/// <summary>
/// Gets the value of the --service command line option.
@@ -19,21 +19,21 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the value of the --package-name command line option.
/// </summary>
string PackageName { get; }
string? PackageName { get; }
/// <summary>
/// Gets the value of the --restartpath command line option.
/// </summary>
string RestartPath { get; }
string? RestartPath { get; }
/// <summary>
/// Gets the value of the --restartargs command line option.
/// </summary>
string RestartArgs { get; }
string? RestartArgs { get; }
/// <summary>
/// Gets the value of the --published-server-url command line option.
/// </summary>
Uri PublishedServerUrl { get; }
string? PublishedServerUrl { get; }
}
}

View File

@@ -1247,7 +1247,7 @@ namespace Emby.Server.Implementations.Library
{
// TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
// https://github.com/dotnet/runtime/issues/20008
if (Enum.TryParse<CollectionTypeOptions>(Path.GetExtension(file), true, out var res))
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
{
return res;
}
@@ -2776,6 +2776,7 @@ namespace Emby.Server.Implementations.Library
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
{
string newPath;
if (ownerItem != null)
{
var libraryOptions = GetLibraryOptions(ownerItem);
@@ -2783,15 +2784,9 @@ namespace Emby.Server.Implementations.Library
{
foreach (var pathInfo in libraryOptions.PathInfos)
{
if (string.IsNullOrWhiteSpace(pathInfo.Path) || string.IsNullOrWhiteSpace(pathInfo.NetworkPath))
if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath))
{
continue;
}
var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
if (substitutionResult.Item2)
{
return substitutionResult.Item1;
return newPath;
}
}
}
@@ -2800,24 +2795,16 @@ namespace Emby.Server.Implementations.Library
var metadataPath = _configurationManager.Configuration.MetadataPath;
var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath))
{
var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath);
if (metadataSubstitutionResult.Item2)
{
return metadataSubstitutionResult.Item1;
}
return newPath;
}
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
{
if (!string.IsNullOrWhiteSpace(map.From))
if (path.TryReplaceSubPath(map.From, map.To, out newPath))
{
var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
if (substitutionResult.Item2)
{
return substitutionResult.Item1;
}
return newPath;
}
}
@@ -2826,47 +2813,12 @@ namespace Emby.Server.Implementations.Library
public string SubstitutePath(string path, string from, string to)
{
return SubstitutePathInternal(path, from, to).Item1;
}
private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
{
if (string.IsNullOrWhiteSpace(path))
if (path.TryReplaceSubPath(from, to, out var newPath))
{
throw new ArgumentNullException(nameof(path));
return newPath;
}
if (string.IsNullOrWhiteSpace(from))
{
throw new ArgumentNullException(nameof(from));
}
if (string.IsNullOrWhiteSpace(to))
{
throw new ArgumentNullException(nameof(to));
}
from = from.Trim();
to = to.Trim();
var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
var changed = false;
if (!string.Equals(newPath, path, StringComparison.Ordinal))
{
if (to.IndexOf('/', StringComparison.Ordinal) != -1)
{
newPath = newPath.Replace('\\', '/');
}
else
{
newPath = newPath.Replace('/', '\\');
}
changed = true;
}
return new Tuple<string, bool>(newPath, changed);
return path;
}
private void SetExtraTypeFromFilename(Video item)
@@ -3001,7 +2953,7 @@ namespace Emby.Server.Implementations.Library
if (collectionType != null)
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString() + ".collection");
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
File.WriteAllBytes(path, Array.Empty<byte>());
}

View File

@@ -1,6 +1,8 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.RegularExpressions;
namespace Emby.Server.Implementations.Library
@@ -47,5 +49,59 @@ namespace Emby.Server.Implementations.Library
return null;
}
/// <summary>
/// Replaces a sub path with another sub path and normalizes the final path.
/// </summary>
/// <param name="path">The original path.</param>
/// <param name="subPath">The original sub path.</param>
/// <param name="newSubPath">The new sub path.</param>
/// <param name="newPath">The result of the sub path replacement</param>
/// <returns>The path after replacing the sub path.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
public static bool TryReplaceSubPath(this string path, string subPath, string newSubPath, [NotNullWhen(true)] out string? newPath)
{
newPath = null;
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(subPath) || string.IsNullOrEmpty(newSubPath) || subPath.Length > path.Length)
{
return false;
}
char oldDirectorySeparatorChar;
char newDirectorySeparatorChar;
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
// The reasoning behind this is that a forward slash likely means it's a Linux path and
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
if (newSubPath.Contains('/', StringComparison.Ordinal))
{
oldDirectorySeparatorChar = '\\';
newDirectorySeparatorChar = '/';
}
else
{
oldDirectorySeparatorChar = '/';
newDirectorySeparatorChar = '\\';
}
path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
// when the sub path matches a similar but in-complete subpath
var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar;
if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase)
|| (!oldSubPathEndsWithSeparator && path[subPath.Length] != newDirectorySeparatorChar))
{
return false;
}
var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar);
// Ensure that the path with the old subpath removed starts with a leading dir separator
int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length;
newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx));
return true;
}
}
}

View File

@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
protected override T Resolve(ItemResolveArgs args)
public override T Resolve(ItemResolveArgs args)
{
return ResolveVideo<T>(args, false);
}
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <param name="args">The args.</param>
/// <param name="parseName">if set to <c>true</c> [parse name].</param>
/// <returns>``0.</returns>
protected TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new()
{
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();

View File

@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
protected override Book Resolve(ItemResolveArgs args)
public override Book Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();

View File

@@ -69,6 +69,110 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result;
}
/// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Video.</returns>
public override Video Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
// Find movies with their own folders
if (args.IsDirectory)
{
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
var files = args.FileSystemChildren
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
.ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
{
// Owned items will be caught by the plain video resolver
if (args.Parent == null)
{
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
return null;
}
if (args.HasParent<Series>())
{
return null;
}
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
return null;
}
// Handle owned items
if (args.Parent == null)
{
return base.Resolve(args);
}
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
Video item = null;
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<MusicVideo>(args, false);
}
// To find a movie file, the collection type must be movies or boxsets
else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Movie>(args, true);
}
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Video>(args, false);
}
else if (string.IsNullOrEmpty(collectionType))
{
if (args.HasParent<Series>())
{
return null;
}
item = ResolveVideo<Video>(args, false);
}
if (item != null)
{
item.IsInMixedFolder = true;
}
return item;
}
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
@@ -216,110 +320,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Video.</returns>
protected override Video Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
// Find movies with their own folders
if (args.IsDirectory)
{
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
var files = args.FileSystemChildren
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
.ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
{
// Owned items will be caught by the plain video resolver
if (args.Parent == null)
{
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
return null;
}
if (args.HasParent<Series>())
{
return null;
}
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
return null;
}
// Handle owned items
if (args.Parent == null)
{
return base.Resolve(args);
}
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
Video item = null;
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<MusicVideo>(args, false);
}
// To find a movie file, the collection type must be movies or boxsets
else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Movie>(args, true);
}
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Video>(args, false);
}
else if (string.IsNullOrEmpty(collectionType))
{
if (args.HasParent<Series>())
{
return null;
}
item = ResolveVideo<Video>(args, false);
}
if (item != null)
{
item.IsInMixedFolder = true;
}
return item;
}
/// <summary>
/// Sets the initial item values.
/// </summary>

View File

@@ -1,5 +1,6 @@
using System;
using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@@ -11,12 +12,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
public class EpisodeResolver : BaseVideoResolver<Episode>
{
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public EpisodeResolver(ILibraryManager libraryManager)
: base(libraryManager)
{
}
/// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Episode.</returns>
protected override Episode Resolve(ItemResolveArgs args)
public override Episode Resolve(ItemResolveArgs args)
{
var parent = args.Parent;
@@ -34,11 +44,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
season = parent.GetParents().OfType<Season>().FirstOrDefault();
}
// If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
// Also handle flat tv folders
if (season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>())
if ((season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>())
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase)))
{
var episode = ResolveVideo<Episode>(args, false);
@@ -74,14 +85,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public EpisodeResolver(ILibraryManager libraryManager)
: base(libraryManager)
{
}
}
}

View File

@@ -45,7 +45,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
{
onStarted();
@@ -70,7 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
onStarted();

View File

@@ -1860,7 +1860,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
@@ -1924,7 +1925,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{

View File

@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -174,7 +174,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -339,7 +339,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,

View File

@@ -204,7 +204,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -219,7 +219,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
@@ -256,7 +256,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -271,7 +271,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
@@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -386,7 +386,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
{
@@ -423,7 +423,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -438,7 +438,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
@@ -534,7 +534,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -549,7 +549,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var cancellationTokenSource = new CancellationTokenSource();
@@ -586,7 +586,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -601,7 +601,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -699,7 +699,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -714,7 +714,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var cancellationTokenSource = new CancellationTokenSource();
@@ -751,7 +751,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -766,7 +766,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -869,7 +869,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -884,7 +884,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var streamingRequest = new VideoRequestDto
@@ -921,7 +921,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -936,7 +936,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -1041,7 +1041,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -1056,7 +1056,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var streamingRequest = new StreamingRequestDto
@@ -1093,7 +1093,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -1108,7 +1108,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};

View File

@@ -63,7 +63,13 @@ namespace Jellyfin.Api.Controllers
{
// TODO: Deprecate with new iOS app
var file = segmentId + Path.GetExtension(Request.Path);
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath))
{
return BadRequest("Invalid segment.");
}
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
}
@@ -83,7 +89,13 @@ namespace Jellyfin.Api.Controllers
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
var file = playlistId + Path.GetExtension(Request.Path);
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8")
{
return BadRequest("Invalid segment.");
}
return GetFileResult(file, file);
}
@@ -98,7 +110,9 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Videos/ActiveEncodings")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId)
public ActionResult StopEncodingProcess(
[FromQuery, Required] string deviceId,
[FromQuery, Required] string playSessionId)
{
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
return NoContent();
@@ -130,7 +144,12 @@ namespace Jellyfin.Api.Controllers
var file = segmentId + Path.GetExtension(Request.Path);
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
file = Path.Combine(transcodeFolderPath, file);
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath))
{
return BadRequest("Invalid segment.");
}
var normalizedPlaylistId = playlistId;

View File

@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers
: type;
var path = BaseItem.SupportedImageExtensions
.Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i))
.Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i)))
.FirstOrDefault(System.IO.File.Exists);
if (path == null)
@@ -82,6 +82,11 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
if (!path.StartsWith(_applicationPaths.GeneralPath))
{
return BadRequest("Invalid image path.");
}
var contentType = MimeTypes.GetMimeType(path);
return File(System.IO.File.OpenRead(path), contentType);
}
@@ -163,7 +168,8 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
private ActionResult GetImageFile(string basePath, string theme, string? name)
{
var themeFolder = Path.Combine(basePath, theme);
var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme));
if (Directory.Exists(themeFolder))
{
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
@@ -171,12 +177,18 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
if (!path.StartsWith(basePath))
{
return BadRequest("Invalid image path.");
}
var contentType = MimeTypes.GetMimeType(path);
return PhysicalFile(path, contentType);
}
}
var allFolder = Path.Combine(basePath, "all");
var allFolder = Path.GetFullPath(Path.Combine(basePath, "all"));
if (Directory.Exists(allFolder))
{
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
@@ -184,6 +196,11 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
if (!path.StartsWith(basePath))
{
return BadRequest("Invalid image path.");
}
var contentType = MimeTypes.GetMimeType(path);
return PhysicalFile(path, contentType);
}

View File

@@ -392,7 +392,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] int newIndex)
[FromQuery, Required] int newIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -741,7 +741,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetArtistImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -820,7 +820,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetGenreImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -900,7 +900,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -978,7 +978,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetMusicGenreImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1058,7 +1058,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1136,7 +1136,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetPersonImage(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -1216,7 +1216,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,

View File

@@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given album.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given playlist.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given genre.
/// </summary>
/// <param name="name">The genre name.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -173,7 +173,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/{name}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName(
[FromRoute, Required] string name,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
@@ -194,7 +194,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given artist.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -230,7 +230,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given genre.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -244,7 +244,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
[FromRoute, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
@@ -266,7 +266,7 @@ namespace Jellyfin.Api.Controllers
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// Creates an instant playlist based on a given item.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -301,6 +301,80 @@ namespace Jellyfin.Api.Controllers
return GetResult(items, user, limit, dtoOptions);
}
/// <summary>
/// Creates an instant playlist based on a given artist.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Artists/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Use GetInstantMixFromArtists")]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2(
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
return GetInstantMixFromArtists(
id,
userId,
limit,
fields,
enableImages,
enableUserData,
imageTypeLimit,
enableImageTypes);
}
/// <summary>
/// Creates an instant playlist based on a given genre.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Obsolete("Use GetInstantMixFromMusicGenres instead")]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById2(
[FromQuery, Required] Guid id,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
return GetInstantMixFromMusicGenreById(
id,
userId,
limit,
fields,
enableImages,
enableUserData,
imageTypeLimit,
enableImageTypes);
}
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
{
var list = items;

View File

@@ -346,11 +346,12 @@ namespace Jellyfin.Api.Controllers
Directory.CreateDirectory(directory);
using (var stream = result.Content)
{
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var fileStream = new FileStream(
fullCachePath,
FileMode.Create,
FileAccess.Write,
FileShare.Read,
FileShare.None,
IODefaults.FileStreamBufferSize,
true);

View File

@@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Items/{itemId}/ContentType")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType)
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)

View File

@@ -304,7 +304,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <response code="204">Library scan started.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpGet("Library/Refresh")]
[HttpPost("Library/Refresh")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RefreshLibrary()
@@ -591,15 +591,15 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Reports that new movies have been added by an external source.
/// </summary>
/// <param name="updates">A list of updated media paths.</param>
/// <param name="dto">The update paths.</param>
/// <response code="204">Report success.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Library/Media/Updated")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates)
public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
{
foreach (var item in updates)
foreach (var item in dto.Updates)
{
_libraryMonitor.ReportFileSystemChanged(item.Path);
}
@@ -778,7 +778,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
[FromQuery] string? libraryContentType,
[FromQuery] bool isNewLibrary)
[FromQuery] bool isNewLibrary = false)
{
var result = new LibraryOptionsResultDto();

View File

@@ -241,23 +241,20 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Updates a media path.
/// </summary>
/// <param name="name">The name of the library.</param>
/// <param name="pathInfo">The path info.</param>
/// <param name="mediaPathRequestDto">The name of the library and path infos.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path updated.</response>
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
[HttpPost("Paths/Update")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateMediaPath(
[FromQuery] string? name,
[FromBody] MediaPathInfo? pathInfo)
public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto)
{
if (string.IsNullOrWhiteSpace(name))
if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name))
{
throw new ArgumentNullException(nameof(name));
throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
}
_libraryManager.UpdateMediaPath(name, pathInfo);
_libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo);
return NoContent();
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using Jellyfin.Api.Constants;
@@ -86,26 +87,19 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Sends a notification to all admins.
/// </summary>
/// <param name="url">The URL of the notification.</param>
/// <param name="level">The level of the notification.</param>
/// <param name="name">The name of the notification.</param>
/// <param name="description">The description of the notification.</param>
/// <param name="notificationDto">The notification request.</param>
/// <response code="204">Notification sent.</response>
/// <returns>A <cref see="NoContentResult"/>.</returns>
[HttpPost("Admin")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CreateAdminNotification(
[FromQuery] string? url,
[FromQuery] NotificationLevel? level,
[FromQuery] string name = "",
[FromQuery] string description = "")
public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto)
{
var notification = new NotificationRequest
{
Name = name,
Description = description,
Url = url,
Level = level ?? NotificationLevel.Normal,
Name = notificationDto.Name,
Description = notificationDto.Description,
Url = notificationDto.Url,
Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal,
UserIds = _userManager.Users
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
.Select(user => user.Id)
@@ -114,7 +108,6 @@ namespace Jellyfin.Api.Controllers
};
_notificationManager.SendNotification(notification, CancellationToken.None);
return NoContent();
}

View File

@@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Sessions/Playing/Ping")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PingPlaybackSession([FromQuery] string playSessionId)
public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
{
_transcodingJobHelper.PingTranscodingJob(playSessionId, null);
return NoContent();
@@ -202,9 +202,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? mediaSourceId,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] PlayMethod playMethod,
[FromQuery] PlayMethod? playMethod,
[FromQuery] string? liveStreamId,
[FromQuery] string playSessionId,
[FromQuery] string? playSessionId,
[FromQuery] bool canSeek = false)
{
var playbackStartInfo = new PlaybackStartInfo
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
MediaSourceId = mediaSourceId,
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
PlayMethod = playMethod,
PlayMethod = playMethod ?? PlayMethod.Transcode,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId
};
@@ -254,10 +254,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? volumeLevel,
[FromQuery] PlayMethod playMethod,
[FromQuery] PlayMethod? playMethod,
[FromQuery] string? liveStreamId,
[FromQuery] string playSessionId,
[FromQuery] RepeatMode repeatMode,
[FromQuery] string? playSessionId,
[FromQuery] RepeatMode? repeatMode,
[FromQuery] bool isPaused = false,
[FromQuery] bool isMuted = false)
{
@@ -271,10 +271,10 @@ namespace Jellyfin.Api.Controllers
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
VolumeLevel = volumeLevel,
PlayMethod = playMethod,
PlayMethod = playMethod ?? PlayMethod.Transcode,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId,
RepeatMode = repeatMode
RepeatMode = repeatMode ?? RepeatMode.RepeatNone
};
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
@@ -352,7 +352,7 @@ namespace Jellyfin.Api.Controllers
return _userDataRepository.GetUserDataDto(item, user);
}
private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
{
if (method == PlayMethod.Transcode)
{

View File

@@ -259,7 +259,8 @@ namespace Jellyfin.Api.Controllers
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
Directory.CreateDirectory(fullCacheDirectory);
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));

View File

@@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] bool? enableRemoteMedia,
[FromQuery] bool breakOnNonKeyFrames,
[FromQuery] bool breakOnNonKeyFrames = false,
[FromQuery] bool enableRedirection = true)
{
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);

View File

@@ -199,7 +199,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
@@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -269,7 +269,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
MaxHeight = maxHeight,
MaxWidth = maxWidth,

View File

@@ -217,9 +217,7 @@ namespace Jellyfin.Api.Controllers
return BadRequest("Please supply at least two videos to merge.");
}
var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList();
var primaryVersion = videosWithVersions.FirstOrDefault();
var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
if (primaryVersion == null)
{
primaryVersion = items
@@ -364,7 +362,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -379,7 +377,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
@@ -418,7 +416,7 @@ namespace Jellyfin.Api.Controllers
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
@@ -433,7 +431,7 @@ namespace Jellyfin.Api.Controllers
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions
};
@@ -620,7 +618,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
@@ -635,7 +633,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions)
{
return GetVideoStream(

View File

@@ -107,7 +107,8 @@ namespace Jellyfin.Api.Helpers
// Headers only
if (isHeadRequest)
{
return new FileContentResult(Array.Empty<byte>(), contentType);
httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
return new OkResult();
}
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);

View File

@@ -508,17 +508,15 @@ namespace Jellyfin.Api.Helpers
private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
{
var headers = request.Headers;
if (!string.IsNullOrWhiteSpace(deviceProfileId))
{
state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
}
else if (!string.IsNullOrWhiteSpace(deviceProfileId))
{
var caps = deviceManager.GetCapabilities(deviceProfileId);
state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile;
if (state.DeviceProfile == null)
{
var caps = deviceManager.GetCapabilities(deviceProfileId);
state.DeviceProfile = caps == null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
}
}
var profile = state.DeviceProfile;

View File

@@ -1,4 +1,7 @@
namespace Jellyfin.Api.Models.LibraryDtos
using System;
using System.Collections.Generic;
namespace Jellyfin.Api.Models.LibraryDtos
{
/// <summary>
/// Media Update Info Dto.
@@ -6,14 +9,8 @@
public class MediaUpdateInfoDto
{
/// <summary>
/// Gets or sets media path.
/// Gets or sets the list of updates.
/// </summary>
public string? Path { get; set; }
/// <summary>
/// Gets or sets media update type.
/// Created, Modified, Deleted.
/// </summary>
public string? UpdateType { get; set; }
public IReadOnlyList<MediaUpdateInfoPathDto> Updates { get; set; } = Array.Empty<MediaUpdateInfoPathDto>();
}
}

View File

@@ -0,0 +1,19 @@
namespace Jellyfin.Api.Models.LibraryDtos
{
/// <summary>
/// The media update info path.
/// </summary>
public class MediaUpdateInfoPathDto
{
/// <summary>
/// Gets or sets media path.
/// </summary>
public string? Path { get; set; }
/// <summary>
/// Gets or sets media update type.
/// Created, Modified, Deleted.
/// </summary>
public string? UpdateType { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
using MediaBrowser.Model.Configuration;
namespace Jellyfin.Api.Models.LibraryStructureDto
{
/// <summary>
/// Update library options dto.
/// </summary>
public class UpdateMediaPathRequestDto
{
/// <summary>
/// Gets or sets the library name.
/// </summary>
[Required]
public string Name { get; set; } = null!;
/// <summary>
/// Gets or sets library folder path information.
/// </summary>
[Required]
public MediaPathInfo PathInfo { get; set; } = null!;
}
}

View File

@@ -0,0 +1,30 @@
using MediaBrowser.Model.Notifications;
namespace Jellyfin.Api.Models.NotificationDtos
{
/// <summary>
/// The admin notification dto.
/// </summary>
public class AdminNotificationDto
{
/// <summary>
/// Gets or sets the notification name.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the notification description.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Gets or sets the notification level.
/// </summary>
public NotificationLevel? NotificationLevel { get; set; }
/// <summary>
/// Gets or sets the notification url.
/// </summary>
public string? Url { get; set; }
}
}

View File

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

View File

@@ -285,14 +285,25 @@ namespace Jellyfin.Networking.Manager
// No bind address and no exclusions, so listen on all interfaces.
Collection<IPObject> result = new Collection<IPObject>();
if (IsIP4Enabled)
if (IsIP6Enabled && IsIP4Enabled)
{
// Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any
result.AddItem(IPAddress.IPv6Any);
}
else if (IsIP4Enabled)
{
result.AddItem(IPAddress.Any);
}
if (IsIP6Enabled)
else if (IsIP6Enabled)
{
result.AddItem(IPAddress.IPv6Any);
// Cannot use IPv6Any as Kestrel will bind to IPv4 addresses.
foreach (var iface in _interfaceAddresses)
{
if (iface.AddressFamily == AddressFamily.InterNetworkV6)
{
result.AddItem(iface.Address);
}
}
}
return result;
@@ -414,7 +425,7 @@ namespace Jellyfin.Networking.Manager
}
// There isn't any others, so we'll use the loopback.
result = IsIP6Enabled ? "::" : "127.0.0.1";
result = IsIP6Enabled ? "::1" : "127.0.0.1";
_logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result);
return result;
}

View File

@@ -21,6 +21,7 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -37,18 +38,21 @@ namespace Jellyfin.Server
/// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
public CoreAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startupConfig,
IFileSystem fileSystem,
IServiceCollection collection)
: base(
applicationPaths,
loggerFactory,
options,
startupConfig,
fileSystem,
collection)
{

View File

@@ -261,15 +261,16 @@ namespace Jellyfin.Server.Extensions
{
return serviceCollection.AddSwaggerGen(c =>
{
var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString() ?? "0.0.0.1";
c.SwaggerDoc("api-docs", new OpenApiInfo
{
Title = "Jellyfin API",
Version = "v1",
Version = version,
Extensions = new Dictionary<string, IOpenApiExtension>
{
{
"x-jellyfin-version",
new OpenApiString(typeof(ApplicationHost).Assembly.GetName().Version?.ToString())
new OpenApiString(version)
}
}
});

View File

@@ -25,6 +25,8 @@ namespace Jellyfin.Server.Filters
context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository);
context.SchemaGenerator.GenerateSchema(typeof(SessionMessageType), context.SchemaRepository);
}
}
}

View File

@@ -164,6 +164,7 @@ namespace Jellyfin.Server
appPaths,
_loggerFactory,
options,
startupConfig,
new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
serviceCollection);
@@ -198,7 +199,7 @@ namespace Jellyfin.Server
}
catch
{
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in system.xml and try again.");
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again.");
throw;
}
@@ -280,7 +281,7 @@ namespace Jellyfin.Server
bool flagged = false;
foreach (IPObject netAdd in addresses)
{
_logger.LogInformation("Kestrel listening on {0}", netAdd);
_logger.LogInformation("Kestrel listening on {Address}", netAdd.Address == IPAddress.IPv6Any ? "All Addresses" : netAdd);
options.Listen(netAdd.Address, appHost.HttpPort);
if (appHost.ListenWithHttps)
{

View File

@@ -77,7 +77,7 @@ namespace Jellyfin.Server
/// <inheritdoc />
[Option("published-server-url", Required = false, HelpText = "Jellyfin Server URL to publish via auto discover process")]
public Uri? PublishedServerUrl { get; set; }
public string? PublishedServerUrl { get; set; }
/// <summary>
/// Gets the command line options as a dictionary that can be used in the .NET configuration system.
@@ -94,7 +94,7 @@ namespace Jellyfin.Server
if (PublishedServerUrl != null)
{
config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl.ToString());
config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl);
}
if (FFmpegPath != null)

View File

@@ -0,0 +1,39 @@
using System;
using System.Buffers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Converter to allow the serializer to read strings.
/// </summary>
public class JsonStringConverter : JsonConverter<string>
{
/// <inheritdoc />
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.String => reader.GetString(),
_ => GetRawValue(reader)
};
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
private static string GetRawValue(Utf8JsonReader reader)
{
var utf8Bytes = reader.HasValueSequence
? reader.ValueSequence.ToArray()
: reader.ValueSpan;
return Encoding.UTF8.GetString(utf8Bytes);
}
}
}

View File

@@ -40,7 +40,8 @@ namespace MediaBrowser.Common.Json
new JsonStringEnumConverter(),
new JsonNullableStructConverterFactory(),
new JsonBoolNumberConverter(),
new JsonDateTimeConverter()
new JsonDateTimeConverter(),
new JsonStringConverter()
}
};

View File

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

View File

@@ -1,6 +1,8 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Library;
namespace MediaBrowser.Controller.Entities.Audio
{
@@ -23,15 +25,7 @@ namespace MediaBrowser.Controller.Entities.Audio
public static IEnumerable<string> GetAllArtists<T>(this T item)
where T : IHasArtist, IHasAlbumArtist
{
foreach (var i in item.AlbumArtists)
{
yield return i;
}
foreach (var i in item.Artists)
{
yield return i;
}
return item.AlbumArtists.Concat(item.Artists).DistinctNames();
}
}
}

View File

@@ -55,7 +55,7 @@ namespace MediaBrowser.Controller
/// <summary>
/// Gets the configured published server url.
/// </summary>
Uri PublishedServerUrl { get; }
string PublishedServerUrl { get; }
/// <summary>
/// Gets the system info.

View File

@@ -10,6 +10,10 @@ namespace MediaBrowser.Controller.Library
{
public static class NameExtensions
{
public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
=> names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First());
private static string RemoveDiacritics(string? name)
{
if (name == null)
@@ -19,9 +23,5 @@ namespace MediaBrowser.Controller.Library
return name.RemoveDiacritics();
}
public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
=> names.GroupBy(RemoveDiacritics, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First());
}
}

View File

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

View File

@@ -15,7 +15,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
protected virtual T Resolve(ItemResolveArgs args)
public virtual T Resolve(ItemResolveArgs args)
{
return null;
}

View File

@@ -133,7 +133,8 @@ namespace MediaBrowser.LocalMetadata.Savers
// On Windows, savint the file will fail if the file is hidden or readonly
FileSystem.SetAttributes(path, false, false);
using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
stream.CopyTo(filestream);
}

View File

@@ -680,7 +680,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (!string.Equals(text, newText, StringComparison.Ordinal))
{
using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))
using (var writer = new StreamWriter(fileStream, encoding))
{
await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);

View File

@@ -0,0 +1,29 @@
#nullable disable
// THIS IS A HACK
// TODO: @bond Move to separate project
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Model.Entities
{
/// <summary>
/// Converts an object to a lowercase string.
/// </summary>
/// <typeparam name="T">The object type.</typeparam>
public class JsonLowerCaseConverter<T> : JsonConverter<T>
{
/// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<T>(ref reader, options);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStringValue(value?.ToString().ToLowerInvariant());
}
}
}

View File

@@ -9,6 +9,33 @@ namespace MediaBrowser.Model.Entities
/// </summary>
public static class ProviderIdsExtensions
{
/// <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)
{
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
return 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)
{
return instance.HasProviderId(provider.ToString());
}
/// <summary>
/// Gets a provider id.
/// </summary>
@@ -29,7 +56,15 @@ namespace MediaBrowser.Model.Entities
return false;
}
return instance.ProviderIds.TryGetValue(name, out id);
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>

View File

@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
using System.Text.Json.Serialization;
using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Model.Entities
@@ -27,6 +28,7 @@ namespace MediaBrowser.Model.Entities
/// Gets or sets the type of the collection.
/// </summary>
/// <value>The type of the collection.</value>
[JsonConverter(typeof(JsonLowerCaseConverter<CollectionTypeOptions?>))]
public CollectionTypeOptions? CollectionType { get; set; }
public LibraryOptions LibraryOptions { get; set; }

View File

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

View File

@@ -18,7 +18,6 @@ namespace MediaBrowser.Model.Notifications
NewLibraryContent,
ServerRestartRequired,
TaskFailed,
CameraImageUploaded,
UserLockedOut
}
}

View File

@@ -263,7 +263,8 @@ namespace MediaBrowser.Providers.Manager
_fileSystem.SetAttributes(path, false, false);
await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
{
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}

View File

@@ -242,6 +242,7 @@ namespace MediaBrowser.Providers.Manager
languages.Add(preferredLanguage);
}
// TODO include [query.IncludeAllLanguages] as an argument to the providers
var tasks = providers.Select(i => GetImages(item, i, languages, cancellationToken, query.ImageType));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
@@ -869,14 +870,14 @@ namespace MediaBrowser.Providers.Manager
}
}
}
catch (Exception)
#pragma warning disable CA1031 // do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // do not catch general exception types
{
// Logged at lower levels
_logger.LogError(ex, "Provider {ProviderName} failed to retrieve search results", provider.Name);
}
}
// _logger.LogDebug("Returning search results {0}", _json.SerializeToString(resultList));
return resultList;
}

View File

@@ -169,7 +169,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}

View File

@@ -153,7 +153,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Directory.CreateDirectory(Path.GetDirectoryName(path));
await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}

View File

@@ -58,7 +58,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
var language = item.GetPreferredMetadataLanguage();
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, null, null, cancellationToken).ConfigureAwait(false);
if (collection?.Images == null)
{

View File

@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -19,11 +20,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly TmdbClientManager _tmdbClientManager;
private readonly ILibraryManager _libraryManager;
public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager, ILibraryManager libraryManager)
{
_httpClientFactory = httpClientFactory;
_tmdbClientManager = tmdbClientManager;
_libraryManager = libraryManager;
}
public string Name => TmdbUtils.ProviderName;
@@ -83,7 +86,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
// We don't already have an Id, need to fetch it
if (tmdbId <= 0)
{
var searchResults = await _tmdbClientManager.SearchCollectionAsync(id.Name, language, cancellationToken).ConfigureAwait(false);
// ParseName is required here.
// Caller provides the filename with extension stripped and NOT the parsed filename
var parsedName = _libraryManager.ParseName(id.Name);
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
var searchResults = await _tmdbClientManager.SearchCollectionAsync(cleanedName, language, cancellationToken).ConfigureAwait(false);
if (searchResults != null && searchResults.Count > 0)
{

View File

@@ -73,8 +73,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return Enumerable.Empty<RemoteImageInfo>();
}
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var movie = await _tmdbClientManager
.GetMovieAsync(movieTmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
.GetMovieAsync(movieTmdbId, null, null, cancellationToken)
.ConfigureAwait(false);
if (movie?.Images == null)

View File

@@ -140,7 +140,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
// ParseName is required here.
// Caller provides the filename with extension stripped and NOT the parsed filename
var parsedName = _libraryManager.ParseName(info.Name);
var searchResults = await _tmdbClientManager.SearchMovieAsync(parsedName.Name, parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (searchResults.Count > 0)
{
@@ -148,6 +149,15 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
}
}
if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId))
{
var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (movieResultFromImdbId?.MovieResults.Count > 0)
{
tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString();
}
}
if (string.IsNullOrEmpty(tmdbId))
{
return new MetadataResult<Movie>();

View File

@@ -63,8 +63,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var language = item.GetPreferredMetadataLanguage();
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var episodeResult = await _tmdbClientManager
.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, null, null, cancellationToken)
.ConfigureAwait(false);
var stills = episodeResult?.Images?.Stills;

View File

@@ -111,10 +111,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var item = new Episode
{
Name = info.Name,
IndexNumber = info.IndexNumber,
ParentIndexNumber = info.ParentIndexNumber,
IndexNumberEnd = info.IndexNumberEnd
IndexNumberEnd = info.IndexNumberEnd,
Name = episodeResult.Name,
PremiereDate = episodeResult.AirDate,
ProductionYear = episodeResult.AirDate?.Year,
Overview = episodeResult.Overview,
CommunityRating = Convert.ToSingle(episodeResult.VoteAverage)
};
if (!string.IsNullOrEmpty(episodeResult.ExternalIds?.TvdbId))
@@ -122,14 +126,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
item.SetProviderId(MetadataProvider.Tvdb, episodeResult.ExternalIds.TvdbId);
}
item.PremiereDate = episodeResult.AirDate;
item.ProductionYear = episodeResult.AirDate?.Year;
item.Name = episodeResult.Name;
item.Overview = episodeResult.Overview;
item.CommunityRating = Convert.ToSingle(episodeResult.VoteAverage);
if (episodeResult.Videos?.Results != null)
{
foreach (var video in episodeResult.Videos.Results)

View File

@@ -52,8 +52,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var language = item.GetPreferredMetadataLanguage();
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var seasonResult = await _tmdbClientManager
.GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
.GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, null, null, cancellationToken)
.ConfigureAwait(false);
var posters = seasonResult?.Images?.Posters;

View File

@@ -59,8 +59,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var language = item.GetPreferredMetadataLanguage();
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var series = await _tmdbClientManager
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), null, null, cancellationToken)
.ConfigureAwait(false);
if (series?.Images == null)

View File

@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -22,15 +23,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly TmdbClientManager _tmdbClientManager;
public TmdbSeriesProvider(
ILibraryManager libraryManager,
IHttpClientFactory httpClientFactory,
TmdbClientManager tmdbClientManager)
{
_libraryManager = libraryManager;
_httpClientFactory = httpClientFactory;
_tmdbClientManager = tmdbClientManager;
Current = this;
}
public string Name => TmdbUtils.ProviderName;
@@ -38,13 +41,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// After TheTVDB
public int Order => 1;
internal static TmdbSeriesProvider Current { get; private set; }
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
if (!string.IsNullOrEmpty(tmdbId))
if (searchInfo.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId))
{
var series = await _tmdbClientManager
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, cancellationToken)
@@ -58,9 +57,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
}
var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrEmpty(imdbId))
if (searchInfo.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
{
var findResult = await _tmdbClientManager
.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, cancellationToken)
@@ -81,9 +78,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
}
var tvdbId = searchInfo.GetProviderId(MetadataProvider.Tvdb);
if (!string.IsNullOrEmpty(tvdbId))
if (searchInfo.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
{
var findResult = await _tmdbClientManager
.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, cancellationToken)
@@ -104,7 +99,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
}
var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken)
var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken: cancellationToken)
.ConfigureAwait(false);
var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
@@ -170,40 +165,32 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
if (string.IsNullOrEmpty(tmdbId))
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
{
var imdbId = info.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrEmpty(imdbId))
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (searchResult?.TvResults.Count > 0)
{
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (searchResult != null)
{
tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture);
}
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
}
if (string.IsNullOrEmpty(tmdbId))
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
{
var tvdbId = info.GetProviderId(MetadataProvider.Tvdb);
if (!string.IsNullOrEmpty(tvdbId))
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (searchResult?.TvResults.Count > 0)
{
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (searchResult != null)
{
tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture);
}
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
}
if (string.IsNullOrEmpty(tmdbId))
{
result.QueriedById = false;
var searchResults = await _tmdbClientManager.SearchSeriesAsync(info.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
// ParseName is required here.
// Caller provides the filename with extension stripped and NOT the parsed filename
var parsedName = _libraryManager.ParseName(info.Name);
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false);
if (searchResults.Count > 0)
{
@@ -211,32 +198,34 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
}
if (!string.IsNullOrEmpty(tmdbId))
if (string.IsNullOrEmpty(tmdbId))
{
cancellationToken.ThrowIfCancellationRequested();
var tvShow = await _tmdbClientManager
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
.ConfigureAwait(false);
result = new MetadataResult<Series>
{
Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode),
ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage
};
foreach (var person in GetPersons(tvShow))
{
result.AddPerson(person);
}
result.HasMetadata = result.Item != null;
return result;
}
cancellationToken.ThrowIfCancellationRequested();
var tvShow = await _tmdbClientManager
.GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
.ConfigureAwait(false);
result = new MetadataResult<Series>
{
Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode),
ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage
};
foreach (var person in GetPersons(tvShow))
{
result.AddPerson(person);
}
result.HasMetadata = result.Item != null;
return result;
}
private Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode)
private static Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode)
{
var series = new Series
{

View File

@@ -278,9 +278,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="name">The name of the tv show.</param>
/// <param name="language">The tv show's language.</param>
/// <param name="year">The year the tv show first aired.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv show information.</returns>
public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, CancellationToken cancellationToken)
public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, int year = 0, CancellationToken cancellationToken = default)
{
var key = $"searchseries-{name}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv> series))
@@ -291,7 +292,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
.SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken)
.SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), firstAirDateYear: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using MediaBrowser.Model.Entities;
using TMDbLib.Objects.General;
@@ -12,6 +13,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
public static class TmdbUtils
{
private static readonly Regex _nonWords = new (@"[\W_]+", RegexOptions.Compiled);
/// <summary>
/// URL of the TMDB instance to use.
/// </summary>
@@ -42,6 +45,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
PersonType.Producer
};
/// <summary>
/// Cleans the name according to TMDb requirements.
/// </summary>
/// <param name="name">The name of the entity.</param>
/// <returns>The cleaned name.</returns>
public static string CleanName(string name)
{
// TMDb expects a space separated list of words make sure that is the case
return _nonWords.Replace(name, " ");
}
/// <summary>
/// Maps the TMDB provided roles for crew members to Jellyfin roles.
/// </summary>

View File

@@ -205,12 +205,30 @@ namespace MediaBrowser.Providers.Subtitles
if (saveInMediaFolder)
{
savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName));
var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName));
// TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path.");
if (mediaFolderPath.StartsWith(video.ContainingFolderPath))
{
savePaths.Add(mediaFolderPath);
}
}
savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
// TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path.");
if (internalPath.StartsWith(video.GetInternalMetadataPath()))
{
savePaths.Add(internalPath);
}
if (savePaths.Count > 0)
{
await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
}
else
{
_logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid.");
}
}
}
@@ -228,7 +246,8 @@ namespace MediaBrowser.Providers.Subtitles
{
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, FileStreamBufferSize, true))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true))
{
await stream.CopyToAsync(fs).ConfigureAwait(false);
}

View File

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

View File

@@ -1,7 +1,7 @@
---
# We just wrap `build` so this is really it
name: "jellyfin"
version: "10.7.0~rc4"
version: "10.7.1"
packages:
- debian.amd64
- debian.arm64

96
debian/changelog vendored
View File

@@ -1,95 +1,11 @@
jellyfin-server (10.7.0~rc4) unstable; urgency=medium
jellyfin-server (10.7.1-1) unstable; urgency=medium
* New upstream version 10.7.0-rc4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc4
* New upstream version 10.7.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.1
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 21 Feb 2021 13:40:42 -0500
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 21 Mar 2021 19:14:11 -0400
jellyfin-server (10.7.0~rc3) unstable; urgency=medium
jellyfin-server (10.7.0-1) unstable; urgency=medium
* New upstream version 10.7.0-rc3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc3
* New upstream version 10.7.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 23 Jan 2021 15:49:02 -0500
jellyfin-server (10.7.0~rc2) unstable; urgency=medium
* New upstream version 10.7.0-rc2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc2
-- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 31 Dec 2020 19:21:59 -0500
jellyfin-server (10.7.0~rc1) unstable; urgency=medium
* New upstream version 10.7.0-rc1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc1
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 04 Dec 2020 21:01:58 -0500
jellyfin-server (10.6.0-2) unstable; urgency=medium
* Fix upgrade bug
-- Joshua Boniface <joshua@boniface.me> Sun, 19 Jul 22:47:27 -0400
jellyfin-server (10.6.0-1) unstable; urgency=medium
* Forthcoming stable release
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 23 Mar 2020 14:46:05 -0400
jellyfin (10.5.0-1) unstable; urgency=medium
* New upstream version 10.5.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.5.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 11 Oct 2019 20:12:38 -0400
jellyfin (10.4.0-1) unstable; urgency=medium
* New upstream version 10.4.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.4.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 31 Aug 2019 21:38:56 -0400
jellyfin (10.3.7-1) unstable; urgency=medium
* New upstream version 10.3.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.7
-- Jellyfin Packaging Team <packaging@jellyfin.org> Wed, 24 Jul 2019 10:48:28 -0400
jellyfin (10.3.6-1) unstable; urgency=medium
* New upstream version 10.3.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.6
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 06 Jul 2019 13:34:19 -0400
jellyfin (10.3.5-1) unstable; urgency=medium
* New upstream version 10.3.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.5
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 09 Jun 2019 21:47:35 -0400
jellyfin (10.3.4-1) unstable; urgency=medium
* New upstream version 10.3.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.4
-- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 06 Jun 2019 22:45:31 -0400
jellyfin (10.3.3-1) unstable; urgency=medium
* New upstream version 10.3.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.3
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 17 May 2019 23:12:08 -0400
jellyfin (10.3.2-1) unstable; urgency=medium
* New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2
-- Jellyfin Packaging Team <packaging@jellyfin.org> Tue, 30 Apr 2019 20:18:44 -0400
jellyfin (10.3.1-1) unstable; urgency=medium
* New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1
-- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 20 Apr 2019 14:24:07 -0400
jellyfin (10.3.0-1) unstable; urgency=medium
* New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 19 Apr 2019 14:24:29 -0400
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 08 Mar 2021 18:09:41 -0500

View File

@@ -5,7 +5,7 @@ Homepage: https://jellyfin.org
Standards-Version: 3.9.2
Package: jellyfin
Version: 10.7.0~rc1
Version: 10.7.1
Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
Depends: jellyfin-server, jellyfin-web
Description: Provides the Jellyfin Free Software Media System

View File

@@ -13,9 +13,7 @@ RUN dnf update -y \
&& dnf install -y @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd
# Install DotNET SDK
RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc \
&& curl -o /etc/yum.repos.d/microsoft-prod.repo https://packages.microsoft.com/config/fedora/$(rpm -E %fedora)/prod.repo \
&& dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION}
RUN dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION}
# Create symlinks and directories
RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora.amd64 /build.sh \

View File

@@ -7,7 +7,7 @@
%endif
Name: jellyfin
Version: 10.7.0~rc4
Version: 10.7.1
Release: 1%{?dist}
Summary: The Free Software Media System
License: GPLv3
@@ -137,35 +137,7 @@ fi
%systemd_postun_with_restart jellyfin.service
%changelog
* Sun Feb 21 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.7.0-rc4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc4
* Sat Jan 23 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.7.0-rc3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc3
* Thu Dec 31 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.7.0-rc2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc2
* Fri Dec 04 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.7.0-rc1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0-rc1
* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- Forthcoming stable release
* Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- Forthcoming stable release
* Fri Oct 11 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.5.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.5.0
* Sat Aug 31 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.4.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.4.0
* Wed Jul 24 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.7
* Sat Jul 06 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.6; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.6
* Sun Jun 09 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.5; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.5
* Thu Jun 06 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.4; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.4
* Fri May 17 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.3
* Tue Apr 30 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.2
* Sat Apr 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1
* Fri Apr 19 2019 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0
* Sun Mar 21 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.7.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.1
* Mon Mar 08 2021 Jellyfin Packaging Team <packaging@jellyfin.org>
- New upstream version 10.7.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.7.0

View File

@@ -8,6 +8,7 @@ using MediaBrowser.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
@@ -77,6 +78,7 @@ namespace Jellyfin.Api.Tests
appPaths,
loggerFactory,
commandLineOpts,
new ConfigurationBuilder().Build(),
new ManagedFileSystem(loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
serviceCollection);
_disposableComponents.Add(appHost);

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Reflection;
using Emby.Server.Implementations;
using Jellyfin.Server;
using MediaBrowser.Controller;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Tests
{
/// <summary>
/// Implementation of the abstract <see cref="ApplicationHost" /> class.
/// </summary>
public class TestAppHost : CoreAppHost
{
/// <summary>
/// Initializes a new instance of the <see cref="TestAppHost" /> class.
/// </summary>
/// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="startup">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param>
/// <param name="collection">The <see cref="IServiceCollection"/> to be used by the <see cref="CoreAppHost"/>.</param>
public TestAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startup,
IFileSystem fileSystem,
IServiceCollection collection)
: base(
applicationPaths,
loggerFactory,
options,
startup,
fileSystem,
collection)
{
}
/// <inheritdoc />
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
{
foreach (var a in base.GetAssembliesWithPartsInternal())
{
yield return a;
}
yield return typeof(TestPlugin).Assembly;
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Text.Json;
using MediaBrowser.Common.Json.Converters;
using Xunit;
namespace Jellyfin.Common.Tests.Json
{
public class JsonStringConverterTests
{
private readonly JsonSerializerOptions _jsonSerializerOptions
= new ()
{
Converters =
{
new JsonStringConverter()
}
};
[Theory]
[InlineData("\"test\"", "test")]
[InlineData("123", "123")]
[InlineData("123.45", "123.45")]
[InlineData("true", "true")]
[InlineData("false", "false")]
public void Deserialize_String_Valid_Success(string input, string output)
{
var deserialized = JsonSerializer.Deserialize<string>(input, _jsonSerializerOptions);
Assert.Equal(deserialized, output);
}
[Fact]
public void Deserialize_Int32asInt32_Valid_Success()
{
const string? input = "123";
const int output = 123;
var deserialized = JsonSerializer.Deserialize<int>(input, _jsonSerializerOptions);
Assert.Equal(deserialized, output);
}
}
}

View File

@@ -0,0 +1,70 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using MediaBrowser.Model.Entities;
using Xunit;
namespace Jellyfin.Model.Tests.Entities
{
public class JsonLowerCaseConverterTests
{
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions()
{
Converters =
{
new JsonStringEnumConverter()
}
};
[Theory]
[InlineData(null, "{\"CollectionType\":null}")]
[InlineData(CollectionTypeOptions.Movies, "{\"CollectionType\":\"movies\"}")]
[InlineData(CollectionTypeOptions.MusicVideos, "{\"CollectionType\":\"musicvideos\"}")]
public void Serialize_CollectionTypeOptions_Correct(CollectionTypeOptions? collectionType, string expected)
{
Assert.Equal(expected, JsonSerializer.Serialize(new TestContainer(collectionType), _jsonOptions));
}
[Theory]
[InlineData("{\"CollectionType\":null}", null)]
[InlineData("{\"CollectionType\":\"movies\"}", CollectionTypeOptions.Movies)]
[InlineData("{\"CollectionType\":\"musicvideos\"}", CollectionTypeOptions.MusicVideos)]
public void Deserialize_CollectionTypeOptions_Correct(string json, CollectionTypeOptions? result)
{
var res = JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions);
Assert.NotNull(res);
Assert.Equal(result, res!.CollectionType);
}
[Theory]
[InlineData(null)]
[InlineData(CollectionTypeOptions.Movies)]
[InlineData(CollectionTypeOptions.MusicVideos)]
public void RoundTrip_CollectionTypeOptions_Correct(CollectionTypeOptions? value)
{
var res = JsonSerializer.Deserialize<TestContainer>(JsonSerializer.Serialize(new TestContainer(value), _jsonOptions), _jsonOptions);
Assert.NotNull(res);
Assert.Equal(value, res!.CollectionType);
}
[Theory]
[InlineData("{\"CollectionType\":null}")]
[InlineData("{\"CollectionType\":\"movies\"}")]
[InlineData("{\"CollectionType\":\"musicvideos\"}")]
public void RoundTrip_String_Correct(string json)
{
var res = JsonSerializer.Serialize(JsonSerializer.Deserialize<TestContainer>(json, _jsonOptions), _jsonOptions);
Assert.Equal(json, res);
}
private class TestContainer
{
public TestContainer(CollectionTypeOptions? collectionType)
{
CollectionType = collectionType;
}
[JsonConverter(typeof(JsonLowerCaseConverter<CollectionTypeOptions?>))]
public CollectionTypeOptions? CollectionType { get; set; }
}
}
}

View File

@@ -9,6 +9,53 @@ namespace Jellyfin.Model.Tests.Entities
{
private const string ExampleImdbId = "tt0113375";
[Fact]
public void HasProviderId_NullInstance_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensions.HasProviderId(null!, MetadataProvider.Imdb));
}
[Fact]
public void HasProviderId_NullProvider_False()
{
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
Assert.False(nullProvider.HasProviderId(MetadataProvider.Imdb));
}
[Fact]
public void HasProviderId_NullName_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => ProviderIdsExtensionsTestsObject.Empty.HasProviderId(null!));
}
[Fact]
public void HasProviderId_NotFoundName_False()
{
Assert.False(ProviderIdsExtensionsTestsObject.Empty.HasProviderId(MetadataProvider.Imdb));
}
[Fact]
public void HasProviderId_FoundName_True()
{
var provider = new ProviderIdsExtensionsTestsObject();
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = ExampleImdbId;
Assert.True(provider.HasProviderId(MetadataProvider.Imdb));
}
[Fact]
public void HasProviderId_FoundNameEmptyValue_False()
{
var provider = new ProviderIdsExtensionsTestsObject();
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = string.Empty;
Assert.False(provider.HasProviderId(MetadataProvider.Imdb));
}
[Fact]
public void GetProviderId_NullInstance_ThrowsArgumentNullException()
{
@@ -30,7 +77,7 @@ namespace Jellyfin.Model.Tests.Entities
[Fact]
public void GetProviderId_NullProvider_Null()
{
var nullProvider = new ProviderIdsExtensionsTestsObject()
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
@@ -47,7 +94,7 @@ namespace Jellyfin.Model.Tests.Entities
[Fact]
public void TryGetProviderId_NullProvider_False()
{
var nullProvider = new ProviderIdsExtensionsTestsObject()
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
@@ -74,6 +121,16 @@ namespace Jellyfin.Model.Tests.Entities
Assert.Equal(ExampleImdbId, id);
}
[Fact]
public void TryGetProviderId_FoundNameEmptyValue_False()
{
var provider = new ProviderIdsExtensionsTestsObject();
provider.ProviderIds[MetadataProvider.Imdb.ToString()] = string.Empty;
Assert.False(provider.TryGetProviderId(MetadataProvider.Imdb, out var id));
Assert.Null(id);
}
[Fact]
public void SetProviderId_NullInstance_ThrowsArgumentNullException()
{
@@ -108,7 +165,7 @@ namespace Jellyfin.Model.Tests.Entities
[Fact]
public void SetProviderId_NullProvider_Success()
{
var nullProvider = new ProviderIdsExtensionsTestsObject()
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};
@@ -120,7 +177,7 @@ namespace Jellyfin.Model.Tests.Entities
[Fact]
public void SetProviderId_NullProviderAndEmptyName_Success()
{
var nullProvider = new ProviderIdsExtensionsTestsObject()
var nullProvider = new ProviderIdsExtensionsTestsObject
{
ProviderIds = null!
};

View File

@@ -28,6 +28,7 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("Crouching.Tiger.Hidden.Dragon.BDrip.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData("Crouching.Tiger.Hidden.Dragon.4K.UltraHD.HDR.BDrip-HDC.mkv", "Crouching.Tiger.Hidden.Dragon")]
[InlineData(null, null)]
// FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
public void CleanStringTest(string input, string expectedName)
{

View File

@@ -327,12 +327,9 @@ namespace Jellyfin.Naming.Tests.Video
FullName = i
}).ToList()).ToList();
Assert.Single(result);
Assert.Equal(7, result.Count);
Assert.Empty(result[0].Extras);
Assert.Equal(6, result[0].AlternateVersions.Count);
Assert.False(result[0].AlternateVersions[2].Is3D);
Assert.True(result[0].AlternateVersions[3].Is3D);
Assert.True(result[0].AlternateVersions[4].Is3D);
Assert.Empty(result[0].AlternateVersions);
}
[Fact]
@@ -406,6 +403,44 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Single(result[0].AlternateVersions);
}
[Fact]
public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName()
{
var files = new[]
{
@"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 1.mkv",
@"/movies/John Wick - Kapitel 3 (2019) [imdbid=tt6146586]/John Wick - Kapitel 3 (2019) [imdbid=tt6146586] - Version 2.mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
Assert.Single(result);
Assert.Empty(result[0].Extras);
Assert.Single(result[0].AlternateVersions);
}
[Fact]
public void Resolve_GivenUnclosedBrackets_DoesNotGroup()
{
var files = new[]
{
@"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 1].mkv",
@"/movies/John Wick - Chapter 3 (2019)/John Wick - Chapter 3 (2019) [Version 2.mkv"
};
var result = _videoListResolver.Resolve(files.Select(i => new FileSystemMetadata
{
IsDirectory = false,
FullName = i
}).ToList()).ToList();
Assert.Equal(2, result.Count);
}
[Fact]
public void TestEmptyList()
{

View File

@@ -0,0 +1,65 @@
using System;
using Emby.Server.Implementations.Library.Resolvers.TV;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library
{
public class EpisodeResolverTest
{
[Fact]
public void Resolve_GivenVideoInExtrasFolder_DoesNotResolveToEpisode()
{
var season = new Season { Name = "Season 1" };
var parent = new Folder { Name = "extras" };
var libraryManagerMock = new Mock<ILibraryManager>();
libraryManagerMock.Setup(x => x.GetItemById(It.IsAny<Guid>())).Returns(season);
var episodeResolver = new EpisodeResolver(libraryManagerMock.Object);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
{
Parent = parent,
CollectionType = CollectionType.TvShows,
Path = "All My Children/Season 01/Extras/All My Children S01E01 - Behind The Scenes.mkv"
};
Assert.Null(episodeResolver.Resolve(itemResolveArgs));
}
[Fact]
public void Resolve_GivenVideoInExtrasSeriesFolder_ResolvesToEpisode()
{
var series = new Series { Name = "Extras" };
// Have to create a mock because of moq proxies not being castable to a concrete implementation
// https://github.com/jellyfin/jellyfin/blob/ab0cff8556403e123642dc9717ba778329554634/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs#L48
var episodeResolver = new EpisodeResolverMock(Mock.Of<ILibraryManager>());
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
Mock.Of<IDirectoryService>())
{
Parent = series,
CollectionType = CollectionType.TvShows,
Path = "Extras/Extras S01E01.mkv"
};
Assert.NotNull(episodeResolver.Resolve(itemResolveArgs));
}
private class EpisodeResolverMock : EpisodeResolver
{
public EpisodeResolverMock(ILibraryManager libraryManager) : base(libraryManager)
{
}
protected override TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) => new ();
}
}
}

View File

@@ -24,5 +24,31 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
Assert.Throws<ArgumentException>(() => PathExtensions.GetAttributeValue(input, attribute));
}
[Theory]
[InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")]
[InlineData("C:/Users/jeff/myfile.mkv", "C:/Users/jeff/", "/home/jeff", "/home/jeff/myfile.mkv")]
[InlineData("/home/jeff/music/jeff's band/consistently inconsistent.mp3", "/home/jeff/music/jeff's band", "/home/not jeff", "/home/not jeff/consistently inconsistent.mp3")]
[InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff", "/home/jeff/myfile.mkv")]
[InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff", "/home/jeff/", "/home/jeff/myfile.mkv")]
[InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/home/jeff/", "/home/jeff/myfile.mkv")]
[InlineData("C:\\Users\\jeff\\myfile.mkv", "C:\\Users/jeff/", "/", "/myfile.mkv")]
public void TryReplaceSubPath_ValidArgs_Correct(string path, string subPath, string newSubPath, string? expectedResult)
{
Assert.True(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result));
Assert.Equal(expectedResult, result);
}
[Theory]
[InlineData("", "", "")]
[InlineData("/my/path", "", "")]
[InlineData("", "/another/path", "")]
[InlineData("", "", "/new/subpath")]
[InlineData("/home/jeff/music/jeff's band/consistently inconsistent.mp3", "/home/jeff/music/not jeff's band", "/home/not jeff")]
public void TryReplaceSubPath_InvalidInput_ReturnsFalseAndNull(string path, string subPath, string newSubPath)
{
Assert.False(PathExtensions.TryReplaceSubPath(path, subPath, newSubPath, out var result));
Assert.Null(result);
}
}
}