Merge branch 'master' into PlugsVersionNumberFix

This commit is contained in:
Claus Vium 2020-12-04 13:50:44 +01:00 committed by GitHub
commit dca3f62ff8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
246 changed files with 8743 additions and 3336 deletions

View File

@ -30,11 +30,11 @@ jobs:
# This is required for the SonarCloud analyzer # This is required for the SonarCloud analyzer
- task: UseDotNet@2 - task: UseDotNet@2
displayName: "Install .NET Core SDK 2.1" displayName: "Install .NET SDK 5.x"
condition: eq(variables['ImageName'], 'ubuntu-latest') condition: eq(variables['ImageName'], 'ubuntu-latest')
inputs: inputs:
packageType: sdk packageType: sdk
version: '2.1.805' version: '5.x'
- task: UseDotNet@2 - task: UseDotNet@2
displayName: "Update DotNet" displayName: "Update DotNet"

36
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
schedule:
- cron: '24 2 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'csharp' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: '5.0.100'
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@ -7,6 +7,7 @@
- [anthonylavado](https://github.com/anthonylavado) - [anthonylavado](https://github.com/anthonylavado)
- [Artiume](https://github.com/Artiume) - [Artiume](https://github.com/Artiume)
- [AThomsen](https://github.com/AThomsen) - [AThomsen](https://github.com/AThomsen)
- [barongreenback](https://github.com/BaronGreenback)
- [barronpm](https://github.com/barronpm) - [barronpm](https://github.com/barronpm)
- [bilde2910](https://github.com/bilde2910) - [bilde2910](https://github.com/bilde2910)
- [bfayers](https://github.com/bfayers) - [bfayers](https://github.com/bfayers)

View File

@ -27,8 +27,15 @@ ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web COPY --from=web-builder /dist /jellyfin/jellyfin-web
# https://github.com/intel/compute-runtime/releases
ARG GMMLIB_VERSION=20.3.2
ARG IGC_VERSION=1.0.5435
ARG NEO_VERSION=20.46.18421
ARG LEVEL_ZERO_VERSION=1.0.18421
# Install dependencies: # Install dependencies:
# mesa-va-drivers: needed for AMD VAAPI # mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \ && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
@ -39,6 +46,20 @@ RUN apt-get update \
jellyfin-ffmpeg \ jellyfin-ffmpeg \
openssl \ openssl \
locales \ locales \
# Intel VAAPI Tone mapping dependencies:
# Prefer NEO to Beignet since the latter one doesn't support Comet Lake or newer for now.
# Do not use the intel-opencl-icd package from repo since they will not build with RELEASE_WITH_REGKEYS enabled.
&& mkdir intel-compute-runtime \
&& cd intel-compute-runtime \
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
&& wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl_${NEO_VERSION}_amd64.deb \
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-ocloc_${NEO_VERSION}_amd64.deb \
&& wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
&& dpkg -i *.deb \
&& cd .. \
&& rm -rf intel-compute-runtime \
&& apt-get remove gnupg wget apt-transport-https -y \ && apt-get remove gnupg wget apt-transport-https -y \
&& apt-get clean autoclean -y \ && apt-get clean autoclean -y \
&& apt-get autoremove -y \ && apt-get autoremove -y \

View File

@ -1681,7 +1681,6 @@ namespace Emby.Dlna.ContentDirectory
private ServerItem GetItemFromObjectId(string id) private ServerItem GetItemFromObjectId(string id)
{ {
return DidlBuilder.IsIdRoot(id) return DidlBuilder.IsIdRoot(id)
? new ServerItem(_libraryManager.GetUserRootFolder()) ? new ServerItem(_libraryManager.GetUserRootFolder())
: ParseItemId(id); : ParseItemId(id);
} }

View File

@ -72,7 +72,8 @@ namespace Emby.Dlna.Eventing
Id = id, Id = id,
CallbackUrl = callbackUrl, CallbackUrl = callbackUrl,
SubscriptionTime = DateTime.UtcNow, SubscriptionTime = DateTime.UtcNow,
TimeoutSeconds = timeout TimeoutSeconds = timeout,
NotificationType = notificationType
}); });
return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout); return GetEventSubscriptionResponse(id, requestedTimeoutString, timeout);

View File

@ -2,12 +2,14 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.PlayTo; using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
@ -134,20 +136,20 @@ namespace Emby.Dlna.Main
{ {
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
await ReloadComponents().ConfigureAwait(false); ReloadComponents();
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
} }
private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
{ {
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
{ {
await ReloadComponents().ConfigureAwait(false); ReloadComponents();
} }
} }
private async Task ReloadComponents() private void ReloadComponents()
{ {
var options = _config.GetDlnaConfiguration(); var options = _config.GetDlnaConfiguration();
@ -155,7 +157,7 @@ namespace Emby.Dlna.Main
if (options.EnableServer) if (options.EnableServer)
{ {
await StartDevicePublisher(options).ConfigureAwait(false); StartDevicePublisher(options);
} }
else else
{ {
@ -225,7 +227,7 @@ namespace Emby.Dlna.Main
} }
} }
public async Task StartDevicePublisher(Configuration.DlnaOptions options) public void StartDevicePublisher(Configuration.DlnaOptions options)
{ {
if (!options.BlastAliveMessages) if (!options.BlastAliveMessages)
{ {
@ -245,7 +247,7 @@ namespace Emby.Dlna.Main
SupportPnpRootDevice = false SupportPnpRootDevice = false
}; };
await RegisterServerEndpoints().ConfigureAwait(false); RegisterServerEndpoints();
_publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
} }
@ -255,14 +257,22 @@ namespace Emby.Dlna.Main
} }
} }
private async Task RegisterServerEndpoints() private void RegisterServerEndpoints()
{ {
var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false);
var udn = CreateUuid(_appHost.SystemId); var udn = CreateUuid(_appHost.SystemId);
var descriptorUri = "/dlna/" + udn + "/description.xml"; var descriptorUri = "/dlna/" + udn + "/description.xml";
foreach (var address in addresses) var bindAddresses = NetworkManager.CreateCollection(
_networkManager.GetInternalBindAddresses()
.Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)));
if (bindAddresses.Count == 0)
{
// No interfaces returned, so use loopback.
bindAddresses = _networkManager.GetLoopbacks();
}
foreach (IPNetAddress address in bindAddresses)
{ {
if (address.AddressFamily == AddressFamily.InterNetworkV6) if (address.AddressFamily == AddressFamily.InterNetworkV6)
{ {
@ -271,7 +281,7 @@ namespace Emby.Dlna.Main
} }
// Limit to LAN addresses only // Limit to LAN addresses only
if (!_networkManager.IsAddressInSubnets(address, true, true)) if (!_networkManager.IsInLocalNetwork(address))
{ {
continue; continue;
} }
@ -280,14 +290,14 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri); var uri = new Uri(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
var device = new SsdpRootDevice var device = new SsdpRootDevice
{ {
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
Location = uri, // Must point to the URL that serves your devices UPnP description document. Location = uri, // Must point to the URL that serves your devices UPnP description document.
Address = address, Address = address.Address,
SubnetMask = _networkManager.GetLocalIpSubnetMask(address), PrefixLength = address.PrefixLength,
FriendlyName = "Jellyfin", FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin", Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server", ModelName = "Jellyfin Server",

View File

@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar namespace Emby.Dlna.MediaReceiverRegistrar
{ {

View File

@ -12,8 +12,6 @@ using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
using Emby.Dlna.Common; using Emby.Dlna.Common;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
@ -345,7 +343,7 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true); RestartTimer(true);
} }
private string CreateDidlMeta(string value) private static string CreateDidlMeta(string value)
{ {
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
{ {
@ -962,7 +960,7 @@ namespace Emby.Dlna.PlayTo
url = "/dmr/" + url; url = "/dmr/" + url;
} }
if (!url.StartsWith("/", StringComparison.Ordinal)) if (!url.StartsWith('/'))
{ {
url = "/" + url; url = "/" + url;
} }

View File

@ -9,7 +9,6 @@ using System.Threading.Tasks;
using Emby.Dlna.Didl; using Emby.Dlna.Didl;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -41,7 +40,6 @@ namespace Emby.Dlna.PlayTo
private readonly IUserDataManager _userDataManager; private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _config;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery; private readonly IDeviceDiscovery _deviceDiscovery;
@ -68,7 +66,6 @@ namespace Emby.Dlna.PlayTo
IUserDataManager userDataManager, IUserDataManager userDataManager,
ILocalizationManager localization, ILocalizationManager localization,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IConfigurationManager config,
IMediaEncoder mediaEncoder) IMediaEncoder mediaEncoder)
{ {
_session = session; _session = session;
@ -84,7 +81,6 @@ namespace Emby.Dlna.PlayTo
_userDataManager = userDataManager; _userDataManager = userDataManager;
_localization = localization; _localization = localization;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_config = config;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
} }
@ -337,25 +333,17 @@ namespace Emby.Dlna.PlayTo
} }
var startIndex = command.StartIndex ?? 0; var startIndex = command.StartIndex ?? 0;
int len = items.Count - startIndex;
if (startIndex > 0) if (startIndex > 0)
{ {
items = items.GetRange(startIndex, items.Count - startIndex); items = items.GetRange(startIndex, len);
} }
var playlist = new List<PlaylistItem>(); var playlist = new PlaylistItem[len];
var isFirst = true; playlist[0] = CreatePlaylistItem(items[0], user, command.StartPositionTicks.Value, command.MediaSourceId, command.AudioStreamIndex, command.SubtitleStreamIndex);
for (int i = 1; i < len; i++)
foreach (var item in items)
{ {
if (isFirst && command.StartPositionTicks.HasValue) playlist[i] = CreatePlaylistItem(items[i], user, 0, null, null, null);
{
playlist.Add(CreatePlaylistItem(item, user, command.StartPositionTicks.Value, command.MediaSourceId, command.AudioStreamIndex, command.SubtitleStreamIndex));
isFirst = false;
}
else
{
playlist.Add(CreatePlaylistItem(item, user, 0, null, null, null));
}
} }
_logger.LogDebug("{0} - Playlist created", _session.DeviceName); _logger.LogDebug("{0} - Playlist created", _session.DeviceName);
@ -468,8 +456,8 @@ namespace Emby.Dlna.PlayTo
_dlnaManager.GetDefaultProfile(); _dlnaManager.GetDefaultProfile();
var mediaSources = item is IHasMediaSources var mediaSources = item is IHasMediaSources
? _mediaSourceManager.GetStaticMediaSources(item, true, user) ? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray()
: new List<MediaSourceInfo>(); : Array.Empty<MediaSourceInfo>();
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex); var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
playlistItem.StreamInfo.StartPositionTicks = startPostionTicks; playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
@ -548,7 +536,7 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
private PlaylistItem GetPlaylistItem(BaseItem item, List<MediaSourceInfo> mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
{ {
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{ {
@ -557,7 +545,7 @@ namespace Emby.Dlna.PlayTo
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions
{ {
ItemId = item.Id, ItemId = item.Id,
MediaSources = mediaSources.ToArray(), MediaSources = mediaSources,
Profile = profile, Profile = profile,
DeviceId = deviceId, DeviceId = deviceId,
MaxBitrate = profile.MaxStreamingBitrate, MaxBitrate = profile.MaxStreamingBitrate,
@ -577,7 +565,7 @@ namespace Emby.Dlna.PlayTo
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions
{ {
ItemId = item.Id, ItemId = item.Id,
MediaSources = mediaSources.ToArray(), MediaSources = mediaSources,
Profile = profile, Profile = profile,
DeviceId = deviceId, DeviceId = deviceId,
MaxBitrate = profile.MaxStreamingBitrate, MaxBitrate = profile.MaxStreamingBitrate,
@ -590,7 +578,7 @@ namespace Emby.Dlna.PlayTo
if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
{ {
return new PlaylistItemFactory().Create((Photo)item, profile); return PlaylistItemFactory.Create((Photo)item, profile);
} }
throw new ArgumentException("Unrecognized item type."); throw new ArgumentException("Unrecognized item type.");
@ -774,13 +762,14 @@ namespace Emby.Dlna.PlayTo
private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken) private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken)
{ {
const int maxWait = 15000000; const int MaxWait = 15000000;
const int interval = 500; const int Interval = 500;
var currentWait = 0; var currentWait = 0;
while (_device.TransportState != TransportState.Playing && currentWait < maxWait) while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
{ {
await Task.Delay(interval).ConfigureAwait(false); await Task.Delay(Interval).ConfigureAwait(false);
currentWait += interval; currentWait += Interval;
} }
await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false); await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);

View File

@ -3,13 +3,11 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
@ -92,10 +90,10 @@ namespace Emby.Dlna.PlayTo
string location = info.Location.ToString(); string location = info.Location.ToString();
// It has to report that it's a media renderer // It has to report that it's a media renderer
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 && if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1) && !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase))
{ {
// _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location); _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
return; return;
} }
@ -130,24 +128,36 @@ namespace Emby.Dlna.PlayTo
} }
} }
private static string GetUuid(string usn) internal static string GetUuid(string usn)
{ {
const string UuidStr = "uuid:"; const string UuidStr = "uuid:";
const string UuidColonStr = "::"; const string UuidColonStr = "::";
var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase); var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase);
if (index != -1) if (index == -1)
{ {
return usn.Substring(index + UuidStr.Length); return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture);
} }
index = usn.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase); ReadOnlySpan<char> tmp = usn.AsSpan()[(index + UuidStr.Length)..];
index = tmp.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase);
if (index != -1) if (index != -1)
{ {
usn = usn.Substring(0, index + UuidColonStr.Length); tmp = tmp[..index];
} }
return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture); index = tmp.IndexOf('{');
if (index != -1)
{
int endIndex = tmp.IndexOf('}');
if (endIndex != -1)
{
tmp = tmp[(index + 1)..endIndex];
}
}
return tmp.ToString();
} }
private async Task AddDevice(UpnpDeviceInfo info, string location, CancellationToken cancellationToken) private async Task AddDevice(UpnpDeviceInfo info, string location, CancellationToken cancellationToken)
@ -177,15 +187,7 @@ namespace Emby.Dlna.PlayTo
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
string serverAddress; string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IPAddress.Any) || info.LocalIpAddress.Equals(IPAddress.IPv6Any))
{
serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
}
else
{
serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress);
}
controller = new PlayToController( controller = new PlayToController(
sessionInfo, sessionInfo,
@ -201,7 +203,6 @@ namespace Emby.Dlna.PlayTo
_userDataManager, _userDataManager,
_localization, _localization,
_mediaSourceManager, _mediaSourceManager,
_config,
_mediaEncoder); _mediaEncoder);
sessionInfo.AddController(controller); sessionInfo.AddController(controller);

View File

@ -8,9 +8,9 @@ using MediaBrowser.Model.Session;
namespace Emby.Dlna.PlayTo namespace Emby.Dlna.PlayTo
{ {
public class PlaylistItemFactory public static class PlaylistItemFactory
{ {
public PlaylistItem Create(Photo item, DeviceProfile profile) public static PlaylistItem Create(Photo item, DeviceProfile profile)
{ {
var playlistItem = new PlaylistItem var playlistItem = new PlaylistItem
{ {

View File

@ -4,7 +4,6 @@ using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -60,7 +59,7 @@ namespace Emby.Dlna.PlayTo
return serviceUrl; return serviceUrl;
} }
if (!serviceUrl.StartsWith("/", StringComparison.Ordinal)) if (!serviceUrl.StartsWith('/'))
{ {
serviceUrl = "/" + serviceUrl; serviceUrl = "/" + serviceUrl;
} }

View File

@ -78,7 +78,7 @@ namespace Emby.Dlna.PlayTo
private static StateVariable FromXml(XElement container) private static StateVariable FromXml(XElement container)
{ {
var allowedValues = new List<string>(); var allowedValues = Array.Empty<string>();
var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList") var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
.FirstOrDefault(); .FirstOrDefault();
@ -86,14 +86,14 @@ namespace Emby.Dlna.PlayTo
{ {
var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue"); var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
allowedValues.AddRange(values.Select(child => child.Value)); allowedValues = values.Select(child => child.Value).ToArray();
} }
return new StateVariable return new StateVariable
{ {
Name = container.GetValue(UPnpNamespaces.Svc + "name"), Name = container.GetValue(UPnpNamespaces.Svc + "name"),
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"), DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
AllowedValues = allowedValues.ToArray() AllowedValues = allowedValues
}; };
} }
@ -103,12 +103,12 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList) foreach (var arg in action.ArgumentList)
{ {
if (arg.Direction == "out") if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
{ {
continue; continue;
} }
if (arg.Name == "InstanceID") if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
{ {
stateString += BuildArgumentXml(arg, "0"); stateString += BuildArgumentXml(arg, "0");
} }
@ -127,12 +127,12 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList) foreach (var arg in action.ArgumentList)
{ {
if (arg.Direction == "out") if (string.Equals(arg.Direction, "out", StringComparison.Ordinal))
{ {
continue; continue;
} }
if (arg.Name == "InstanceID") if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
{ {
stateString += BuildArgumentXml(arg, "0"); stateString += BuildArgumentXml(arg, "0");
} }
@ -151,7 +151,7 @@ namespace Emby.Dlna.PlayTo
foreach (var arg in action.ArgumentList) foreach (var arg in action.ArgumentList)
{ {
if (arg.Name == "InstanceID") if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal))
{ {
stateString += BuildArgumentXml(arg, "0"); stateString += BuildArgumentXml(arg, "0");
} }

View File

@ -1,5 +1,6 @@
using System.Reflection; using System.Reflection;
using System.Resources; using System.Resources;
using System.Runtime.CompilerServices;
// General Information about an assembly is controlled through the following // General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information // set of attributes. Change these attribute values to modify the information
@ -13,6 +14,7 @@ using System.Resources;
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")] [assembly: NeutralResourcesLanguage("en")]
[assembly: InternalsVisibleTo("Jellyfin.Dlna.Tests")]
// Version information for an assembly consists of the following four values: // Version information for an assembly consists of the following four values:
// //

View File

@ -40,8 +40,6 @@ namespace Emby.Dlna.Server
_serverId = serverId; _serverId = serverId;
} }
private static bool EnableAbsoluteUrls => false;
public string GetXml() public string GetXml()
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
@ -75,13 +73,6 @@ namespace Emby.Dlna.Server
builder.Append("<minor>0</minor>"); builder.Append("<minor>0</minor>");
builder.Append("</specVersion>"); builder.Append("</specVersion>");
if (!EnableAbsoluteUrls)
{
builder.Append("<URLBase>")
.Append(SecurityElement.Escape(_serverAddress))
.Append("</URLBase>");
}
AppendDeviceInfo(builder); AppendDeviceInfo(builder);
builder.Append("</root>"); builder.Append("</root>");
@ -257,14 +248,7 @@ namespace Emby.Dlna.Server
return string.Empty; return string.Empty;
} }
url = url.TrimStart('/'); url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
url = "/dlna/" + _serverUdn + "/" + url;
if (EnableAbsoluteUrls)
{
url = _serverAddress.TrimEnd('/') + url;
}
return SecurityElement.Escape(url); return SecurityElement.Escape(url);
} }

View File

@ -227,7 +227,11 @@ namespace Emby.Naming.Video
testFilename = cleanName.ToString(); testFilename = cleanName.ToString();
} }
testFilename = testFilename.Substring(folderName.Length).Trim(); if (folderName.Length <= testFilename.Length)
{
testFilename = testFilename.Substring(folderName.Length).Trim();
}
return string.IsNullOrEmpty(testFilename) return string.IsNullOrEmpty(testFilename)
|| testFilename[0].Equals('-') || testFilename[0].Equals('-')
|| testFilename[0].Equals('_') || testFilename[0].Equals('_')

View File

@ -1,14 +1,12 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Sockets;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
@ -46,10 +44,11 @@ using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV; using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates; using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events; using MediaBrowser.Common.Events;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
@ -82,7 +81,6 @@ using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV; using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
@ -97,6 +95,7 @@ using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.Tmdb; using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles; using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers; using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -117,14 +116,12 @@ namespace Emby.Server.Implementations
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
private readonly IFileSystem _fileSystemManager; private readonly IFileSystem _fileSystemManager;
private readonly INetworkManager _networkManager;
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions; private readonly IStartupOptions _startupOptions;
private IMediaEncoder _mediaEncoder; private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager; private ISessionManager _sessionManager;
private IHttpClientFactory _httpClientFactory;
private string[] _urlPrefixes; private string[] _urlPrefixes;
/// <summary> /// <summary>
@ -158,6 +155,11 @@ namespace Emby.Server.Implementations
} }
} }
/// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary>
public INetworkManager NetManager { get; internal set; }
/// <summary> /// <summary>
/// Occurs when [has pending restart changed]. /// Occurs when [has pending restart changed].
/// </summary> /// </summary>
@ -212,7 +214,7 @@ namespace Emby.Server.Implementations
private readonly List<IDisposable> _disposableParts = new List<IDisposable>(); private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
/// <summary> /// <summary>
/// Gets the configuration manager. /// Gets or sets the configuration manager.
/// </summary> /// </summary>
/// <value>The configuration manager.</value> /// <value>The configuration manager.</value>
protected IConfigurationManager ConfigurationManager { get; set; } protected IConfigurationManager ConfigurationManager { get; set; }
@ -245,14 +247,12 @@ namespace Emby.Server.Implementations
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> 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="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param> /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost( public ApplicationHost(
IServerApplicationPaths applicationPaths, IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IStartupOptions options, IStartupOptions options,
IFileSystem fileSystem, IFileSystem fileSystem,
INetworkManager networkManager,
IServiceCollection serviceCollection) IServiceCollection serviceCollection)
{ {
_xmlSerializer = new MyXmlSerializer(); _xmlSerializer = new MyXmlSerializer();
@ -260,14 +260,17 @@ namespace Emby.Server.Implementations
ServiceCollection = serviceCollection; ServiceCollection = serviceCollection;
_networkManager = networkManager;
networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
ApplicationPaths = applicationPaths; ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
_fileSystemManager = fileSystem; _fileSystemManager = fileSystem;
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
// Have to migrate settings here as migration subsystem not yet initialised.
MigrateNetworkConfiguration();
// Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
Logger = LoggerFactory.CreateLogger<ApplicationHost>(); Logger = LoggerFactory.CreateLogger<ApplicationHost>();
@ -281,8 +284,6 @@ namespace Emby.Server.Implementations
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
_networkManager.NetworkChanged += OnNetworkChanged;
CertificateInfo = new CertificateInfo CertificateInfo = new CertificateInfo
{ {
Path = ServerConfigurationManager.Configuration.CertificatePath, Path = ServerConfigurationManager.Configuration.CertificatePath,
@ -295,6 +296,22 @@ namespace Emby.Server.Implementations
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
} }
/// <summary>
/// Temporary function to migration network settings out of system.xml and into network.xml.
/// TODO: remove at the point when a fixed migration path has been decided upon.
/// </summary>
private void MigrateNetworkConfiguration()
{
string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
if (!File.Exists(path))
{
var networkSettings = new NetworkConfiguration();
ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
_xmlSerializer.SerializeToFile(networkSettings, path);
Logger?.LogDebug("Successfully migrated network settings.");
}
}
public string ExpandVirtualPath(string path) public string ExpandVirtualPath(string path)
{ {
var appPaths = ApplicationPaths; var appPaths = ApplicationPaths;
@ -311,16 +328,6 @@ namespace Emby.Server.Implementations
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase); .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
} }
private string[] GetConfiguredLocalSubnets()
{
return ServerConfigurationManager.Configuration.LocalNetworkSubnets;
}
private void OnNetworkChanged(object sender, EventArgs e)
{
_validAddressResults.Clear();
}
/// <inheritdoc /> /// <inheritdoc />
public Version ApplicationVersion { get; } public Version ApplicationVersion { get; }
@ -487,14 +494,15 @@ namespace Emby.Server.Implementations
/// <inheritdoc/> /// <inheritdoc/>
public void Init() public void Init()
{ {
HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; HttpPort = networkConfiguration.HttpServerPortNumber;
HttpsPort = networkConfiguration.HttpsPortNumber;
// Safeguard against invalid configuration // Safeguard against invalid configuration
if (HttpPort == HttpsPort) if (HttpPort == HttpsPort)
{ {
HttpPort = ServerConfiguration.DefaultHttpPort; HttpPort = NetworkConfiguration.DefaultHttpPort;
HttpsPort = ServerConfiguration.DefaultHttpsPort; HttpsPort = NetworkConfiguration.DefaultHttpsPort;
} }
DiscoverTypes(); DiscoverTypes();
@ -523,7 +531,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(_fileSystemManager); ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TmdbClientManager>(); ServiceCollection.AddSingleton<TmdbClientManager>();
ServiceCollection.AddSingleton(_networkManager); ServiceCollection.AddSingleton(NetManager);
ServiceCollection.AddSingleton<IIsoManager, IsoManager>(); ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
@ -627,7 +635,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
ServiceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
ServiceCollection.AddSingleton<EncodingHelper>(); ServiceCollection.AddSingleton<EncodingHelper>();
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
@ -649,7 +656,6 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>(); _mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>(); _sessionManager = Resolve<ISessionManager>();
_httpClientFactory = Resolve<IHttpClientFactory>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
@ -770,8 +776,6 @@ namespace Emby.Server.Implementations
if (Plugins != null) if (Plugins != null)
{ {
var pluginBuilder = new StringBuilder();
foreach (var plugin in Plugins) foreach (var plugin in Plugins)
{ {
if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin) if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin)
@ -791,13 +795,8 @@ namespace Emby.Server.Implementations
} }
} }
pluginBuilder.Append(plugin.Name) Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
.Append(' ')
.Append(plugin.Version)
.AppendLine();
} }
Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
} }
_urlPrefixes = GetUrlPrefixes().ToArray(); _urlPrefixes = GetUrlPrefixes().ToArray();
@ -921,9 +920,10 @@ namespace Emby.Server.Implementations
// Don't do anything if these haven't been set yet // Don't do anything if these haven't been set yet
if (HttpPort != 0 && HttpsPort != 0) if (HttpPort != 0 && HttpsPort != 0)
{ {
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
// Need to restart if ports have changed // Need to restart if ports have changed
if (ServerConfigurationManager.Configuration.HttpServerPortNumber != HttpPort || if (networkConfiguration.HttpServerPortNumber != HttpPort ||
ServerConfigurationManager.Configuration.HttpsPortNumber != HttpsPort) networkConfiguration.HttpsPortNumber != HttpsPort)
{ {
if (ServerConfigurationManager.Configuration.IsPortAuthorized) if (ServerConfigurationManager.Configuration.IsPortAuthorized)
{ {
@ -1053,7 +1053,7 @@ namespace Emby.Server.Implementations
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1]; metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_'); int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion)) if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion))
{ {
// Versioned folder. // Versioned folder.
versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir)); versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
@ -1167,6 +1167,9 @@ namespace Emby.Server.Implementations
// Xbmc // Xbmc
yield return typeof(ArtistNfoProvider).Assembly; yield return typeof(ArtistNfoProvider).Assembly;
// Network
yield return typeof(NetworkManager).Assembly;
foreach (var i in GetAssembliesWithPartsInternal()) foreach (var i in GetAssembliesWithPartsInternal())
{ {
yield return i; yield return i;
@ -1178,13 +1181,10 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Gets the system status. /// Gets the system status.
/// </summary> /// </summary>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="source">Where this request originated.</param>
/// <returns>SystemInfo.</returns> /// <returns>SystemInfo.</returns>
public async Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken) public SystemInfo GetSystemInfo(IPAddress source)
{ {
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
var transcodingTempPath = ConfigurationManager.GetTranscodePath();
return new SystemInfo return new SystemInfo
{ {
HasPendingRestart = HasPendingRestart, HasPendingRestart = HasPendingRestart,
@ -1204,9 +1204,9 @@ namespace Emby.Server.Implementations
CanSelfRestart = CanSelfRestart, CanSelfRestart = CanSelfRestart,
CanLaunchWebBrowser = CanLaunchWebBrowser, CanLaunchWebBrowser = CanLaunchWebBrowser,
HasUpdateAvailable = HasUpdateAvailable, HasUpdateAvailable = HasUpdateAvailable,
TranscodingTempPath = transcodingTempPath, TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName, ServerName = FriendlyName,
LocalAddress = localAddress, LocalAddress = GetSmartApiUrl(source),
SupportsLibraryMonitor = true, SupportsLibraryMonitor = true,
EncoderLocation = _mediaEncoder.EncoderLocation, EncoderLocation = _mediaEncoder.EncoderLocation,
SystemArchitecture = RuntimeInformation.OSArchitecture, SystemArchitecture = RuntimeInformation.OSArchitecture,
@ -1215,14 +1215,12 @@ namespace Emby.Server.Implementations
} }
public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo() public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo()
=> _networkManager.GetMacAddresses() => NetManager.GetMacAddresses()
.Select(i => new WakeOnLanInfo(i)) .Select(i => new WakeOnLanInfo(i))
.ToList(); .ToList();
public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken) public PublicSystemInfo GetPublicSystemInfo(IPAddress source)
{ {
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
return new PublicSystemInfo return new PublicSystemInfo
{ {
Version = ApplicationVersionString, Version = ApplicationVersionString,
@ -1230,195 +1228,100 @@ namespace Emby.Server.Implementations
Id = SystemId, Id = SystemId,
OperatingSystem = OperatingSystem.Id.ToString(), OperatingSystem = OperatingSystem.Id.ToString(),
ServerName = FriendlyName, ServerName = FriendlyName,
LocalAddress = localAddress, LocalAddress = GetSmartApiUrl(source),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
}; };
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
/// <inheritdoc/> /// <inheritdoc/>
public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken) public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
{ {
try // Published server ends with a /
if (_startupOptions.PublishedServerUrl != null)
{ {
// Return the first matched address, if found, or the first known local address // Published server ends with a '/', so we need to remove it.
var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false); return _startupOptions.PublishedServerUrl.ToString().Trim('/');
if (addresses.Count == 0)
{
return null;
}
return GetLocalApiUrl(addresses[0]);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error getting local Ip address information");
} }
return null; string smart = NetManager.GetBindInterface(ipAddress, out port);
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return smart.Trim('/');
}
return GetLocalApiUrl(smart.Trim('/'), null, port);
} }
/// <summary> /// <inheritdoc/>
/// Removes the scope id from IPv6 addresses. public string GetSmartApiUrl(HttpRequest request, int? port = null)
/// </summary>
/// <param name="address">The IPv6 address.</param>
/// <returns>The IPv6 address without the scope id.</returns>
private ReadOnlySpan<char> RemoveScopeId(ReadOnlySpan<char> address)
{ {
var index = address.IndexOf('%'); // Published server ends with a /
if (index == -1) if (_startupOptions.PublishedServerUrl != null)
{ {
return address; // Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
} }
return address.Slice(0, index); string smart = NetManager.GetBindInterface(request, out port);
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return smart.Trim('/');
}
return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
} }
/// <inheritdoc /> /// <inheritdoc/>
public string GetLocalApiUrl(IPAddress ipAddress) public string GetSmartApiUrl(string hostname, int? port = null)
{ {
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) // Published server ends with a /
if (_startupOptions.PublishedServerUrl != null)
{ {
var str = RemoveScopeId(ipAddress.ToString()); // Published server ends with a '/', so we need to remove it.
Span<char> span = new char[str.Length + 2]; return _startupOptions.PublishedServerUrl.ToString().Trim('/');
span[0] = '[';
str.CopyTo(span.Slice(1));
span[^1] = ']';
return GetLocalApiUrl(span);
} }
return GetLocalApiUrl(ipAddress.ToString()); string smart = NetManager.GetBindInterface(hostname, out port);
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return smart.Trim('/');
}
return GetLocalApiUrl(smart.Trim('/'), null, port);
} }
/// <inheritdoc/> /// <inheritdoc/>
public string GetLoopbackHttpApiUrl() public string GetLoopbackHttpApiUrl()
{ {
if (NetManager.IsIP6Enabled)
{
return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort);
}
return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort); return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
} }
/// <inheritdoc/> /// <inheritdoc/>
public string GetLocalApiUrl(ReadOnlySpan<char> host, string scheme = null, int? port = null) public string GetLocalApiUrl(string host, string scheme = null, int? port = null)
{ {
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
// not. For consistency, always trim the trailing slash. // not. For consistency, always trim the trailing slash.
return new UriBuilder return new UriBuilder
{ {
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
Host = host.ToString(), Host = host,
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
Path = ServerConfigurationManager.Configuration.BaseUrl Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
}.ToString().TrimEnd('/'); }.ToString().TrimEnd('/');
} }
public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
{
return GetLocalIpAddressesInternal(true, 0, cancellationToken);
}
private async Task<List<IPAddress>> GetLocalIpAddressesInternal(bool allowLoopback, int limit, CancellationToken cancellationToken)
{
var addresses = ServerConfigurationManager
.Configuration
.LocalNetworkAddresses
.Select(x => NormalizeConfiguredLocalAddress(x))
.Where(i => i != null)
.ToList();
if (addresses.Count == 0)
{
addresses.AddRange(_networkManager.GetLocalIpAddresses());
}
var resultList = new List<IPAddress>();
foreach (var address in addresses)
{
if (!allowLoopback)
{
if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback))
{
continue;
}
}
if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false))
{
resultList.Add(address);
if (limit > 0 && resultList.Count >= limit)
{
return resultList;
}
}
}
return resultList;
}
public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address)
{
var index = address.Trim('/').IndexOf('/');
if (index != -1)
{
address = address.Slice(index + 1);
}
if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))
{
return result;
}
return null;
}
private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
private async Task<bool> IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
{
if (address.Equals(IPAddress.Loopback)
|| address.Equals(IPAddress.IPv6Loopback))
{
return true;
}
var apiUrl = GetLocalApiUrl(address) + "/system/ping";
if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult))
{
return cachedResult;
}
try
{
using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var result = await System.Text.Json.JsonSerializer.DeserializeAsync<string>(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false);
var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
_validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
return valid;
}
catch (OperationCanceledException)
{
Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, "Cancelled");
throw;
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Ping test result to {0}. Success: {1}", apiUrl, false);
_validAddressResults.AddOrUpdate(apiUrl, false, (k, v) => false);
return false;
}
}
public string FriendlyName => public string FriendlyName =>
string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName) string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName)
? Environment.MachineName ? Environment.MachineName

View File

@ -540,18 +540,18 @@ namespace Emby.Server.Implementations.Channels
{ {
IncludeItemTypes = new[] { nameof(Channel) }, IncludeItemTypes = new[] { nameof(Channel) },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) } OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
}).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray(); }).Select(i => GetChannelFeatures(i)).ToArray();
} }
/// <inheritdoc /> /// <inheritdoc />
public ChannelFeatures GetChannelFeatures(string id) public ChannelFeatures GetChannelFeatures(Guid? id)
{ {
if (string.IsNullOrEmpty(id)) if (!id.HasValue)
{ {
throw new ArgumentNullException(nameof(id)); throw new ArgumentNullException(nameof(id));
} }
var channel = GetChannel(id); var channel = GetChannel(id.Value);
var channelProvider = GetChannelProvider(channel); var channelProvider = GetChannelProvider(channel);
return GetChannelFeaturesDto(channel, channelProvider, channelProvider.GetChannelFeatures()); return GetChannelFeaturesDto(channel, channelProvider, channelProvider.GetChannelFeatures());
@ -634,7 +634,7 @@ namespace Emby.Server.Implementations.Channels
{ {
var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray(); var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray();
if (query.ChannelIds.Length > 0) if (query.ChannelIds.Count > 0)
{ {
// Avoid implicitly captured closure // Avoid implicitly captured closure
var ids = query.ChannelIds; var ids = query.ChannelIds;

View File

@ -3611,12 +3611,12 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add($"type in ({inClause})"); whereClauses.Add($"type in ({inClause})");
} }
if (query.ChannelIds.Length == 1) if (query.ChannelIds.Count == 1)
{ {
whereClauses.Add("ChannelId=@ChannelId"); whereClauses.Add("ChannelId=@ChannelId");
statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture));
} }
else if (query.ChannelIds.Length > 1) else if (query.ChannelIds.Count > 1)
{ {
var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
whereClauses.Add($"ChannelId in ({inClause})"); whereClauses.Add($"ChannelId in ({inClause})");
@ -4076,7 +4076,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(clause); whereClauses.Add(clause);
} }
if (query.GenreIds.Length > 0) if (query.GenreIds.Count > 0)
{ {
var clauses = new List<string>(); var clauses = new List<string>();
var index = 0; var index = 0;
@ -4097,7 +4097,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(clause); whereClauses.Add(clause);
} }
if (query.Genres.Length > 0) if (query.Genres.Count > 0)
{ {
var clauses = new List<string>(); var clauses = new List<string>();
var index = 0; var index = 0;
@ -4519,17 +4519,17 @@ namespace Emby.Server.Implementations.Data
if (query.HasImdbId.HasValue) if (query.HasImdbId.HasValue)
{ {
whereClauses.Add("ProviderIds like '%imdb=%'"); whereClauses.Add(GetProviderIdClause(query.HasImdbId.Value, "imdb"));
} }
if (query.HasTmdbId.HasValue) if (query.HasTmdbId.HasValue)
{ {
whereClauses.Add("ProviderIds like '%tmdb=%'"); whereClauses.Add(GetProviderIdClause(query.HasTmdbId.Value, "tmdb"));
} }
if (query.HasTvdbId.HasValue) if (query.HasTvdbId.HasValue)
{ {
whereClauses.Add("ProviderIds like '%tvdb=%'"); whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb"));
} }
var includedItemByNameTypes = GetItemByNameTypesInQuery(query).SelectMany(MapIncludeItemTypes).ToList(); var includedItemByNameTypes = GetItemByNameTypesInQuery(query).SelectMany(MapIncludeItemTypes).ToList();
@ -4769,6 +4769,21 @@ namespace Emby.Server.Implementations.Data
return whereClauses; return whereClauses;
} }
/// <summary>
/// Formats a where clause for the specified provider.
/// </summary>
/// <param name="includeResults">Whether or not to include items with this provider's ids.</param>
/// <param name="provider">Provider name.</param>
/// <returns>Formatted SQL clause.</returns>
private string GetProviderIdClause(bool includeResults, string provider)
{
return string.Format(
CultureInfo.InvariantCulture,
"ProviderIds {0} like '%{1}=%'",
includeResults ? string.Empty : "not",
provider);
}
private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query) private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
{ {
var list = new List<string>(); var list = new List<string>();

View File

@ -1,61 +1,38 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices; using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.Extensions.Caching.Memory;
namespace Emby.Server.Implementations.Devices namespace Emby.Server.Implementations.Devices
{ {
public class DeviceManager : IDeviceManager public class DeviceManager : IDeviceManager
{ {
private readonly IMemoryCache _memoryCache;
private readonly IJsonSerializer _json;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IServerConfigurationManager _config;
private readonly IAuthenticationRepository _authRepo; private readonly IAuthenticationRepository _authRepo;
private readonly object _capabilitiesSyncLock = new object(); private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager)
public DeviceManager(
IAuthenticationRepository authRepo,
IJsonSerializer json,
IUserManager userManager,
IServerConfigurationManager config,
IMemoryCache memoryCache)
{ {
_json = json;
_userManager = userManager; _userManager = userManager;
_config = config;
_memoryCache = memoryCache;
_authRepo = authRepo; _authRepo = authRepo;
} }
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities) public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
{ {
var path = Path.Combine(GetDevicePath(deviceId), "capabilities.json"); _capabilitiesMap[deviceId] = capabilities;
Directory.CreateDirectory(Path.GetDirectoryName(path));
lock (_capabilitiesSyncLock)
{
_memoryCache.Set(deviceId, capabilities);
_json.SerializeToFile(capabilities, path);
}
} }
public void UpdateDeviceOptions(string deviceId, DeviceOptions options) public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
@ -72,32 +49,12 @@ namespace Emby.Server.Implementations.Devices
public ClientCapabilities GetCapabilities(string id) public ClientCapabilities GetCapabilities(string id)
{ {
if (_memoryCache.TryGetValue(id, out ClientCapabilities result)) return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
{ ? result
return result; : new ClientCapabilities();
}
lock (_capabilitiesSyncLock)
{
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
try
{
return _json.DeserializeFromFile<ClientCapabilities>(path) ?? new ClientCapabilities();
}
catch
{
}
}
return new ClientCapabilities();
} }
public DeviceInfo GetDevice(string id) public DeviceInfo GetDevice(string id)
{
return GetDevice(id, true);
}
private DeviceInfo GetDevice(string id, bool includeCapabilities)
{ {
var session = _authRepo.Get(new AuthenticationInfoQuery var session = _authRepo.Get(new AuthenticationInfoQuery
{ {
@ -154,16 +111,6 @@ namespace Emby.Server.Implementations.Devices
}; };
} }
private string GetDevicesPath()
{
return Path.Combine(_config.ApplicationPaths.DataPath, "devices");
}
private string GetDevicePath(string id)
{
return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture));
}
public bool CanAccessDevice(User user, string deviceId) public bool CanAccessDevice(User user, string deviceId)
{ {
if (user == null) if (user == null)

View File

@ -22,7 +22,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="IPNetwork2" Version="2.5.226" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" /> <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
@ -37,8 +36,8 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Mono.Nat" Version="3.0.1" /> <PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.10.0" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.10.2" />
<PackageReference Include="sharpcompress" Version="0.26.0" /> <PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.0" /> <PackageReference Include="DotNet.Glob" Version="3.1.0" />

View File

@ -8,6 +8,7 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
@ -56,7 +57,7 @@ namespace Emby.Server.Implementations.EntryPoints
private string GetConfigIdentifier() private string GetConfigIdentifier()
{ {
const char Separator = '|'; const char Separator = '|';
var config = _config.Configuration; var config = _config.GetNetworkConfiguration();
return new StringBuilder(32) return new StringBuilder(32)
.Append(config.EnableUPnP).Append(Separator) .Append(config.EnableUPnP).Append(Separator)
@ -93,7 +94,8 @@ namespace Emby.Server.Implementations.EntryPoints
private void Start() private void Start()
{ {
if (!_config.Configuration.EnableUPnP || !_config.Configuration.EnableRemoteAccess) var config = _config.GetNetworkConfiguration();
if (!config.EnableUPnP || !config.EnableRemoteAccess)
{ {
return; return;
} }
@ -156,11 +158,12 @@ namespace Emby.Server.Implementations.EntryPoints
private IEnumerable<Task> CreatePortMaps(INatDevice device) private IEnumerable<Task> CreatePortMaps(INatDevice device)
{ {
yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort); var config = _config.GetNetworkConfiguration();
yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPort);
if (_appHost.ListenWithHttps) if (_appHost.ListenWithHttps)
{ {
yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort); yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort);
} }
} }

View File

@ -1,6 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>(); private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>(); private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>(); private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
private readonly Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>(); private readonly ConcurrentDictionary<Guid, DateTime> _lastProgressMessageTimes = new ConcurrentDictionary<Guid, DateTime>();
public LibraryChangedNotifier( public LibraryChangedNotifier(
ILibraryManager libraryManager, ILibraryManager libraryManager,
@ -98,7 +99,7 @@ namespace Emby.Server.Implementations.EntryPoints
} }
} }
_lastProgressMessageTimes[item.Id] = DateTime.UtcNow; _lastProgressMessageTimes.AddOrUpdate(item.Id, key => DateTime.UtcNow, (key, existing) => DateTime.UtcNow);
var dict = new Dictionary<string, string>(); var dict = new Dictionary<string, string>();
dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture); dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture);
@ -140,6 +141,8 @@ namespace Emby.Server.Implementations.EntryPoints
private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e) private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
{ {
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100))); OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
_lastProgressMessageTimes.TryRemove(e.Argument.Id, out DateTime removed);
} }
private static bool EnableRefreshMessage(BaseItem item) private static bool EnableRefreshMessage(BaseItem item)

View File

@ -1,5 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
@ -20,9 +21,15 @@ namespace Emby.Server.Implementations.HttpServer.Security
public AuthorizationInfo Authenticate(HttpRequest request) public AuthorizationInfo Authenticate(HttpRequest request)
{ {
var auth = _authorizationContext.GetAuthorizationInfo(request); var auth = _authorizationContext.GetAuthorizationInfo(request);
if (!auth.HasToken)
{
throw new AuthenticationException("Request does not contain a token.");
}
if (!auth.IsAuthenticated) if (!auth.IsAuthenticated)
{ {
throw new AuthenticationException("Invalid token."); throw new SecurityException("Invalid token.");
} }
if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false) if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)

View File

@ -102,7 +102,8 @@ namespace Emby.Server.Implementations.HttpServer.Security
DeviceId = deviceId, DeviceId = deviceId,
Version = version, Version = version,
Token = token, Token = token,
IsAuthenticated = false IsAuthenticated = false,
HasToken = false
}; };
if (string.IsNullOrWhiteSpace(token)) if (string.IsNullOrWhiteSpace(token))
@ -111,6 +112,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
return authInfo; return authInfo;
} }
authInfo.HasToken = true;
var result = _authRepo.Get(new AuthenticationInfoQuery var result = _authRepo.Get(new AuthenticationInfoQuery
{ {
AccessToken = token AccessToken = token

View File

@ -0,0 +1,130 @@
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library
{
/// <summary>
/// A library post scan/refresh task for pre-fetching remote images.
/// </summary>
public class ImageFetcherPostScanTask : ILibraryPostScanTask
{
private readonly ILibraryManager _libraryManager;
private readonly IProviderManager _providerManager;
private readonly ILogger<ImageFetcherPostScanTask> _logger;
private readonly SemaphoreSlim _imageFetcherLock;
private ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)> _queuedItems;
/// <summary>
/// Initializes a new instance of the <see cref="ImageFetcherPostScanTask"/> class.
/// </summary>
/// <param name="libraryManager">An instance of <see cref="ILibraryManager"/>.</param>
/// <param name="providerManager">An instance of <see cref="IProviderManager"/>.</param>
/// <param name="logger">An instance of <see cref="ILogger{ImageFetcherPostScanTask}"/>.</param>
public ImageFetcherPostScanTask(
ILibraryManager libraryManager,
IProviderManager providerManager,
ILogger<ImageFetcherPostScanTask> logger)
{
_libraryManager = libraryManager;
_providerManager = providerManager;
_logger = logger;
_queuedItems = new ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)>();
_imageFetcherLock = new SemaphoreSlim(1, 1);
_libraryManager.ItemAdded += OnLibraryManagerItemAddedOrUpdated;
_libraryManager.ItemUpdated += OnLibraryManagerItemAddedOrUpdated;
_providerManager.RefreshCompleted += OnProviderManagerRefreshCompleted;
}
/// <inheritdoc />
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
// Sometimes a library scan will cause this to run twice if there's an item refresh going on.
await _imageFetcherLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var now = DateTime.UtcNow;
var itemGuids = _queuedItems.Keys.ToList();
for (var i = 0; i < itemGuids.Count; i++)
{
if (!_queuedItems.TryGetValue(itemGuids[i], out var queuedItem))
{
continue;
}
var itemId = queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture);
var itemType = queuedItem.item.GetType();
_logger.LogDebug(
"Updating remote images for item {ItemId} with media type {ItemMediaType}",
itemId,
itemType);
try
{
await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch images for {Type} item with id {ItemId}", itemType, itemId);
}
_queuedItems.TryRemove(queuedItem.item.Id, out _);
}
if (itemGuids.Count > 0)
{
_logger.LogInformation(
"Finished updating/pre-fetching {NumberOfImages} images. Elapsed time: {TimeElapsed}s.",
itemGuids.Count.ToString(CultureInfo.InvariantCulture),
(DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
}
else
{
_logger.LogDebug("No images were updated.");
}
}
finally
{
_imageFetcherLock.Release();
}
}
private void OnLibraryManagerItemAddedOrUpdated(object sender, ItemChangeEventArgs itemChangeEventArgs)
{
if (!_queuedItems.ContainsKey(itemChangeEventArgs.Item.Id) && itemChangeEventArgs.Item.ImageInfos.Length > 0)
{
_queuedItems.AddOrUpdate(
itemChangeEventArgs.Item.Id,
(itemChangeEventArgs.Item, itemChangeEventArgs.UpdateReason),
(key, existingValue) => existingValue);
}
}
private void OnProviderManagerRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
{
if (!_queuedItems.ContainsKey(e.Argument.Id) && e.Argument.ImageInfos.Length > 0)
{
_queuedItems.AddOrUpdate(
e.Argument.Id,
(e.Argument, ItemUpdateType.None),
(key, existingValue) => existingValue);
}
// The RefreshCompleted event is a bit awkward in that it seems to _only_ be fired on
// the item that was refreshed regardless of children refreshes. So we take it as a signal
// that the refresh is entirely completed.
Run(null, CancellationToken.None).GetAwaiter().GetResult();
}
}
}

View File

@ -858,7 +858,21 @@ namespace Emby.Server.Implementations.Library
/// <returns>Task{Person}.</returns> /// <returns>Task{Person}.</returns>
public Person GetPerson(string name) public Person GetPerson(string name)
{ {
return CreateItemByName<Person>(Person.GetPath, name, new DtoOptions(true)); var path = Person.GetPath(name);
var id = GetItemByNameId<Person>(path);
if (!(GetItemById(id) is Person item))
{
item = new Person
{
Name = name,
Id = id,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
Path = path
};
}
return item;
} }
/// <summary> /// <summary>
@ -1503,7 +1517,7 @@ namespace Emby.Server.Implementations.Library
{ {
if (query.AncestorIds.Length == 0 && if (query.AncestorIds.Length == 0 &&
query.ParentId.Equals(Guid.Empty) && query.ParentId.Equals(Guid.Empty) &&
query.ChannelIds.Length == 0 && query.ChannelIds.Count == 0 &&
query.TopParentIds.Length == 0 && query.TopParentIds.Length == 0 &&
string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) && string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
@ -1941,19 +1955,9 @@ namespace Emby.Server.Implementations.Library
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) public Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{ {
foreach (var item in items) RunMetadataSavers(items, updateReason);
{
if (item.IsFileProtocol)
{
ProviderManager.SaveMetadata(item, updateReason);
}
item.DateLastSaved = DateTime.UtcNow;
await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
}
_itemRepository.SaveItems(items, cancellationToken); _itemRepository.SaveItems(items, cancellationToken);
@ -1984,12 +1988,27 @@ namespace Emby.Server.Implementations.Library
} }
} }
} }
return Task.CompletedTask;
} }
/// <inheritdoc /> /// <inheritdoc />
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken); => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
public void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason)
{
foreach (var item in items)
{
if (item.IsFileProtocol)
{
ProviderManager.SaveMetadata(item, updateReason);
}
item.DateLastSaved = DateTime.UtcNow;
}
}
/// <summary> /// <summary>
/// Reports the item removed. /// Reports the item removed.
/// </summary> /// </summary>
@ -2443,9 +2462,19 @@ namespace Emby.Server.Implementations.Library
public BaseItem GetParentItem(string parentId, Guid? userId) public BaseItem GetParentItem(string parentId, Guid? userId)
{ {
if (!string.IsNullOrEmpty(parentId)) if (string.IsNullOrEmpty(parentId))
{ {
return GetItemById(new Guid(parentId)); return GetParentItem((Guid?)null, userId);
}
return GetParentItem(new Guid(parentId), userId);
}
public BaseItem GetParentItem(Guid? parentId, Guid? userId)
{
if (parentId.HasValue)
{
return GetItemById(parentId.Value);
} }
if (userId.HasValue && userId != Guid.Empty) if (userId.HasValue && userId != Guid.Empty)

View File

@ -156,8 +156,8 @@ namespace Emby.Server.Implementations.Library
ExcludeItemTypes = excludeItemTypes.ToArray(), ExcludeItemTypes = excludeItemTypes.ToArray(),
IncludeItemTypes = includeItemTypes.ToArray(), IncludeItemTypes = includeItemTypes.ToArray(),
Limit = query.Limit, Limit = query.Limit,
IncludeItemsByName = string.IsNullOrEmpty(query.ParentId), IncludeItemsByName = !query.ParentId.HasValue,
ParentId = string.IsNullOrEmpty(query.ParentId) ? Guid.Empty : new Guid(query.ParentId), ParentId = query.ParentId ?? Guid.Empty,
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
Recursive = true, Recursive = true,

View File

@ -4,7 +4,6 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@ -19,7 +18,6 @@ using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -36,6 +34,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly IApplicationHost _appHost; private readonly IApplicationHost _appHost;
private readonly ICryptoProvider _cryptoProvider; private readonly ICryptoProvider _cryptoProvider;
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private DateTime _lastErrorResponse;
public SchedulesDirect( public SchedulesDirect(
ILogger<SchedulesDirect> logger, ILogger<SchedulesDirect> logger,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
@ -50,8 +51,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_cryptoProvider = cryptoProvider; _cryptoProvider = cryptoProvider;
} }
private string UserAgent => _appHost.ApplicationUserAgent;
/// <inheritdoc /> /// <inheritdoc />
public string Name => "Schedules Direct"; public string Name => "Schedules Direct";
@ -307,7 +306,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
if (details.contentRating != null && details.contentRating.Count > 0) if (details.contentRating != null && details.contentRating.Count > 0)
{ {
info.OfficialRating = details.contentRating[0].code.Replace("TV", "TV-").Replace("--", "-"); info.OfficialRating = details.contentRating[0].code.Replace("TV", "TV-", StringComparison.Ordinal)
.Replace("--", "-", StringComparison.Ordinal);
var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" }; var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" };
if (invalid.Contains(info.OfficialRating, StringComparer.OrdinalIgnoreCase)) if (invalid.Contains(info.OfficialRating, StringComparer.OrdinalIgnoreCase))
@ -450,7 +450,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms( private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
ListingsProviderInfo info, ListingsProviderInfo info,
List<string> programIds, IReadOnlyList<string> programIds,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (programIds.Count == 0) if (programIds.Count == 0)
@ -458,23 +458,21 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return new List<ScheduleDirect.ShowImages>(); return new List<ScheduleDirect.ShowImages>();
} }
var imageIdString = "["; StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
foreach (ReadOnlySpan<char> i in programIds)
foreach (var i in programIds)
{ {
var imageId = i.Substring(0, 10); str.Append('"')
.Append(i.Slice(0, 10))
if (!imageIdString.Contains(imageId, StringComparison.Ordinal)) .Append("\",");
{
imageIdString += "\"" + imageId + "\",";
}
} }
imageIdString = imageIdString.TrimEnd(',') + "]"; // Remove last ,
str.Length--;
str.Append(']');
using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs") using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs")
{ {
Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json) Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json)
}; };
try try
@ -539,9 +537,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return lineups; return lineups;
} }
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private DateTime _lastErrorResponse;
private async Task<string> GetToken(ListingsProviderInfo info, CancellationToken cancellationToken) private async Task<string> GetToken(ListingsProviderInfo info, CancellationToken cancellationToken)
{ {
var username = info.Username; var username = info.Username;
@ -564,8 +559,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return null; return null;
} }
NameValuePair savedToken; if (!_tokens.TryGetValue(username, out NameValuePair savedToken))
if (!_tokens.TryGetValue(username, out savedToken))
{ {
savedToken = new NameValuePair(); savedToken = new NameValuePair();
_tokens.TryAdd(username, savedToken); _tokens.TryAdd(username, savedToken);
@ -647,13 +641,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{ {
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>()); var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
string hashedPassword = Hex.Encode(hashedPasswordBytes); // TODO: remove ToLower when Convert.ToHexString supports lowercase
// Schedules Direct requires the hex to be lowercase
string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant();
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json); options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false); var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
if (root.message == "OK") if (string.Equals(root.message, "OK", StringComparison.Ordinal))
{ {
_logger.LogInformation("Authenticated with Schedules Direct token: " + root.token); _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
return root.token; return root.token;
@ -777,24 +773,28 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId); using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId);
options.Headers.TryAddWithoutValidation("token", token); options.Headers.TryAddWithoutValidation("token", token);
var list = new List<ChannelInfo>();
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false); var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false);
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count); _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
_logger.LogInformation("Mapping Stations to Channel"); _logger.LogInformation("Mapping Stations to Channel");
var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>(); var allStations = root.stations ?? new List<ScheduleDirect.Station>();
foreach (ScheduleDirect.Map map in root.map) var map = root.map;
int len = map.Count;
var array = new List<ChannelInfo>(len);
for (int i = 0; i < len; i++)
{ {
var channelNumber = GetChannelNumber(map); var channelNumber = GetChannelNumber(map[i]);
var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase)); var station = allStations.Find(item => string.Equals(item.stationID, map[i].stationID, StringComparison.OrdinalIgnoreCase));
if (station == null) if (station == null)
{ {
station = new ScheduleDirect.Station { stationID = map.stationID }; station = new ScheduleDirect.Station
{
stationID = map[i].stationID
};
} }
var channelInfo = new ChannelInfo var channelInfo = new ChannelInfo
@ -810,32 +810,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
channelInfo.ImageUrl = station.logo.URL; channelInfo.ImageUrl = station.logo.URL;
} }
list.Add(channelInfo); array[i] = channelInfo;
} }
return list; return array;
}
private ScheduleDirect.Station GetStation(List<ScheduleDirect.Station> allStations, string channelNumber, string channelName)
{
if (!string.IsNullOrWhiteSpace(channelName))
{
channelName = NormalizeName(channelName);
var result = allStations.FirstOrDefault(i => string.Equals(NormalizeName(i.callsign ?? string.Empty), channelName, StringComparison.OrdinalIgnoreCase));
if (result != null)
{
return result;
}
}
if (!string.IsNullOrWhiteSpace(channelNumber))
{
return allStations.FirstOrDefault(i => string.Equals(NormalizeName(i.stationID ?? string.Empty), channelNumber, StringComparison.OrdinalIgnoreCase));
}
return null;
} }
private static string NormalizeName(string value) private static string NormalizeName(string value)
@ -1044,7 +1022,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
} }
} }
//
public class Title public class Title
{ {
public string title120 { get; set; } public string title120 { get; set; }

View File

@ -76,7 +76,6 @@ namespace Emby.Server.Implementations.LiveTv
} }
var list = sources.ToList(); var list = sources.ToList();
var serverUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
foreach (var source in list) foreach (var source in list)
{ {
@ -103,7 +102,7 @@ namespace Emby.Server.Implementations.LiveTv
// Dummy this up so that direct play checks can still run // Dummy this up so that direct play checks can still run
if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http) if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
{ {
source.Path = serverUrl; source.Path = _appHost.GetSmartApiUrl(string.Empty);
} }
} }

View File

@ -237,8 +237,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
if (!inside) if (!inside)
{ {
buffer[bufferIndex] = let; buffer[bufferIndex++] = let;
bufferIndex++;
} }
} }

View File

@ -111,11 +111,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
{ {
using (var client = new TcpClient(new IPEndPoint(remoteIp, HdHomeRunPort))) using var client = new TcpClient();
using (var stream = client.GetStream()) client.Connect(remoteIp, HdHomeRunPort);
{
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); using var stream = client.GetStream();
} return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
} }
private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken) private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
@ -142,7 +142,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
_remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort); _remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort);
_tcpClient = new TcpClient(_remoteEndPoint); _tcpClient = new TcpClient();
_tcpClient.Connect(_remoteEndPoint);
if (!_lockkey.HasValue) if (!_lockkey.HasValue)
{ {
@ -221,30 +222,30 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return; return;
} }
using (var tcpClient = new TcpClient(_remoteEndPoint)) using var tcpClient = new TcpClient();
using (var stream = tcpClient.GetStream()) tcpClient.Connect(_remoteEndPoint);
{
var commandList = commands.GetCommands();
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
foreach (var command in commandList)
{
var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey);
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked using var stream = tcpClient.GetStream();
if (!ParseReturnMessage(buffer, receivedBytes, out _)) var commandList = commands.GetCommands();
{ byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
return; try
} {
foreach (var command in commandList)
{
var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey);
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _))
{
return;
} }
} }
finally }
{ finally
ArrayPool<byte>.Shared.Return(buffer); {
} ArrayPool<byte>.Shared.Return(buffer);
} }
} }

View File

@ -3,7 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -50,6 +52,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
EnableStreamSharing = true; EnableStreamSharing = true;
} }
/// <summary>
/// Returns an unused UDP port number in the range specified.
/// Temporarily placed here until future network PR merged.
/// </summary>
/// <param name="range">Upper and Lower boundary of ports to select.</param>
/// <returns>System.Int32.</returns>
private static int GetUdpPortFromRange((int Min, int Max) range)
{
var properties = IPGlobalProperties.GetIPGlobalProperties();
// Get active udp listeners.
var udpListenerPorts = properties.GetActiveUdpListeners()
.Where(n => n.Port >= range.Min && n.Port <= range.Max)
.Select(n => n.Port);
return Enumerable
.Range(range.Min, range.Max)
.FirstOrDefault(i => !udpListenerPorts.Contains(i));
}
public override async Task Open(CancellationToken openCancellationToken) public override async Task Open(CancellationToken openCancellationToken)
{ {
LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
@ -57,7 +79,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var mediaSource = OriginalMediaSource; var mediaSource = OriginalMediaSource;
var uri = new Uri(mediaSource.Path); var uri = new Uri(mediaSource.Path);
var localPort = _networkManager.GetRandomUnusedUdpPort(); // Temporary code to reduce PR size. This will be updated by a future network pr.
var localPort = GetUdpPortFromRange((49152, 65535));
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
@ -70,7 +93,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
try try
{ {
await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false); await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false);
localAddress = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address; localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address;
tcpClient.Close(); tcpClient.Close();
} }
catch (Exception ex) catch (Exception ex)
@ -80,6 +103,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
} }
if (localAddress.IsIPv4MappedToIPv6) {
localAddress = localAddress.MapToIPv4();
}
var udpClient = new UdpClient(localPort, AddressFamily.InterNetwork); var udpClient = new UdpClient(localPort, AddressFamily.InterNetwork);
var hdHomerunManager = new HdHomerunManager(); var hdHomerunManager = new HdHomerunManager();
@ -110,12 +137,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var taskCompletionSource = new TaskCompletionSource<bool>(); var taskCompletionSource = new TaskCompletionSource<bool>();
await StartStreaming( _ = StartStreaming(
udpClient, udpClient,
hdHomerunManager, hdHomerunManager,
remoteAddress, remoteAddress,
taskCompletionSource, taskCompletionSource,
LiveStreamCancellationTokenSource.Token).ConfigureAwait(false); LiveStreamCancellationTokenSource.Token);
// OpenedMediaSource.Protocol = MediaProtocol.File; // OpenedMediaSource.Protocol = MediaProtocol.File;
// OpenedMediaSource.Path = tempFile; // OpenedMediaSource.Path = tempFile;
@ -136,33 +163,30 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return TempFilePath; return TempFilePath;
} }
private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{ {
return Task.Run(async () => using (udpClient)
using (hdHomerunManager)
{ {
using (udpClient) try
using (hdHomerunManager)
{ {
try await CopyTo(udpClient, TempFilePath, openTaskCompletionSource, cancellationToken).ConfigureAwait(false);
{ }
await CopyTo(udpClient, TempFilePath, openTaskCompletionSource, cancellationToken).ConfigureAwait(false); catch (OperationCanceledException ex)
} {
catch (OperationCanceledException ex) Logger.LogInformation("HDHR UDP stream cancelled or timed out from {0}", remoteAddress);
{ openTaskCompletionSource.TrySetException(ex);
Logger.LogInformation("HDHR UDP stream cancelled or timed out from {0}", remoteAddress); }
openTaskCompletionSource.TrySetException(ex); catch (Exception ex)
} {
catch (Exception ex) Logger.LogError(ex, "Error opening live stream:");
{ openTaskCompletionSource.TrySetException(ex);
Logger.LogError(ex, "Error opening live stream:");
openTaskCompletionSource.TrySetException(ex);
}
EnableStreamSharing = false;
} }
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false); EnableStreamSharing = false;
}); }
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
} }
private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)

View File

@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
extInf = line.Substring(ExtInfPrefix.Length).Trim(); extInf = line.Substring(ExtInfPrefix.Length).Trim();
_logger.LogInformation("Found m3u channel: {0}", extInf); _logger.LogInformation("Found m3u channel: {0}", extInf);
} }
else if (!string.IsNullOrWhiteSpace(extInf) && !line.StartsWith("#", StringComparison.OrdinalIgnoreCase)) else if (!string.IsNullOrWhiteSpace(extInf) && !line.StartsWith('#'))
{ {
var channel = GetChannelnfo(extInf, tunerHostId, line); var channel = GetChannelnfo(extInf, tunerHostId, line);
if (string.IsNullOrWhiteSpace(channel.Id)) if (string.IsNullOrWhiteSpace(channel.Id))

View File

@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var extension = "ts"; var extension = "ts";
var requiresRemux = false; var requiresRemux = false;
var contentType = response.Content.Headers.ContentType.ToString(); var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1) if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1)
{ {
requiresRemux = true; requiresRemux = true;

View File

@ -113,5 +113,10 @@
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.", "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.",
"TaskCleanTranscode": "Rengør Transcode Mappen", "TaskCleanTranscode": "Rengør Transcode Mappen",
"TaskRefreshPeople": "Genopfrisk Personer", "TaskRefreshPeople": "Genopfrisk Personer",
"TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek." "TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigureret alder.",
"TaskCleanActivityLog": "Ryd Aktivitetslog",
"Undefined": "Udefineret",
"Forced": "Tvunget",
"Default": "Standard"
} }

View File

@ -113,5 +113,10 @@
"TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή", "TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή",
"TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.", "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.",
"TaskUpdatePlugins": "Ενημέρωση Προσθηκών", "TaskUpdatePlugins": "Ενημέρωση Προσθηκών",
"TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας." "TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας.",
"TaskCleanActivityLogDescription": "Διαγράφει καταχωρήσεις απο το αρχείο καταγραφής παλαιότερες από την επιλεγμένη ηλικία.",
"TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων",
"Undefined": "Απροσδιόριστο",
"Forced": "Εξαναγκασμένο",
"Default": "Προεπιλογή"
} }

View File

@ -113,5 +113,9 @@
"TasksChannelsCategory": "Canales de Internet", "TasksChannelsCategory": "Canales de Internet",
"TasksApplicationCategory": "Aplicación", "TasksApplicationCategory": "Aplicación",
"TasksLibraryCategory": "Biblioteca", "TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Mantenimiento" "TasksMaintenanceCategory": "Mantenimiento",
"TaskCleanActivityLogDescription": "Elimina entradas del registro de actividad que sean más antiguas al periodo establecido.",
"TaskCleanActivityLog": "Limpiar registro de actividades",
"Undefined": "Sin definir",
"Forced": "Forzado"
} }

View File

@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} falló", "ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciada", "ScheduledTaskStartedWithName": "{0} iniciada",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Mostrar", "Shows": "Series de Televisión",
"Songs": "Canciones", "Songs": "Canciones",
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.", "StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}", "SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}",

View File

@ -112,5 +112,7 @@
"TaskCleanCache": "Tyhjennä välimuisti-hakemisto", "TaskCleanCache": "Tyhjennä välimuisti-hakemisto",
"TasksChannelsCategory": "Internet kanavat", "TasksChannelsCategory": "Internet kanavat",
"TasksApplicationCategory": "Sovellus", "TasksApplicationCategory": "Sovellus",
"TasksLibraryCategory": "Kirjasto" "TasksLibraryCategory": "Kirjasto",
"Forced": "Pakotettu",
"Default": "Oletus"
} }

View File

@ -113,5 +113,6 @@
"TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires", "TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires",
"TasksApplicationCategory": "Application", "TasksApplicationCategory": "Application",
"TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système.", "TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système.",
"TasksChannelsCategory": "Canaux Internet" "TasksChannelsCategory": "Canaux Internet",
"Default": "Par défaut"
} }

View File

@ -93,8 +93,8 @@
"ValueSpecialEpisodeName": "Spécial - {0}", "ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}", "VersionNumber": "Version {0}",
"TasksChannelsCategory": "Chaines en ligne", "TasksChannelsCategory": "Chaines en ligne",
"TaskDownloadMissingSubtitlesDescription": "Cherche les sous-titres manquant sur internet en se basant sur la configuration des métadonnées.", "TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquants sur internet en se basant sur la configuration des métadonnées.",
"TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquant", "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants",
"TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.", "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.",
"TaskRefreshChannels": "Rafraîchir les chaines", "TaskRefreshChannels": "Rafraîchir les chaines",
"TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.", "TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.",

View File

@ -115,5 +115,8 @@
"TaskRefreshChannels": "Csatornák frissítése", "TaskRefreshChannels": "Csatornák frissítése",
"TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.", "TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.",
"TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.", "TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.",
"TaskCleanActivityLog": "Tevékenységnapló törlése" "TaskCleanActivityLog": "Tevékenységnapló törlése",
"Undefined": "Meghatározatlan",
"Forced": "Kényszerített",
"Default": "Alapértelmezett"
} }

View File

@ -112,5 +112,10 @@
"TaskRefreshPeople": "Muat ulang Orang", "TaskRefreshPeople": "Muat ulang Orang",
"TaskCleanLogsDescription": "Menghapus file log yang lebih dari {0} hari.", "TaskCleanLogsDescription": "Menghapus file log yang lebih dari {0} hari.",
"TaskCleanLogs": "Bersihkan Log Direktori", "TaskCleanLogs": "Bersihkan Log Direktori",
"TaskRefreshLibrary": "Pindai Pustaka Media" "TaskRefreshLibrary": "Pindai Pustaka Media",
"TaskCleanActivityLogDescription": "Menghapus log aktivitas yang lebih tua dari umur yang dikonfigurasi.",
"TaskCleanActivityLog": "Bersihkan Log Aktivitas",
"Undefined": "Tidak terdefinisi",
"Forced": "Dipaksa",
"Default": "Bawaan"
} }

View File

@ -115,5 +115,8 @@
"TasksLibraryCategory": "Libreria", "TasksLibraryCategory": "Libreria",
"TasksMaintenanceCategory": "Manutenzione", "TasksMaintenanceCategory": "Manutenzione",
"TaskCleanActivityLog": "Attività di Registro Completate", "TaskCleanActivityLog": "Attività di Registro Completate",
"TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie delletà configurata." "TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie delletà configurata.",
"Undefined": "Non Definito",
"Forced": "Forzato",
"Default": "Predefinito"
} }

View File

@ -87,7 +87,7 @@
"UserOnlineFromDevice": "{0} heeft verbinding met {1}", "UserOnlineFromDevice": "{0} heeft verbinding met {1}",
"UserPasswordChangedWithName": "Wachtwoord voor {0} is gewijzigd", "UserPasswordChangedWithName": "Wachtwoord voor {0} is gewijzigd",
"UserPolicyUpdatedWithName": "Gebruikersbeleid gewijzigd voor {0}", "UserPolicyUpdatedWithName": "Gebruikersbeleid gewijzigd voor {0}",
"UserStartedPlayingItemWithValues": "{0} heeft afspelen van {1} gestart op {2}", "UserStartedPlayingItemWithValues": "{0} speelt {1} af op {2}",
"UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt op {2}", "UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt op {2}",
"ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek", "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek",
"ValueSpecialEpisodeName": "Speciaal - {0}", "ValueSpecialEpisodeName": "Speciaal - {0}",
@ -115,5 +115,8 @@
"TasksLibraryCategory": "Bibliotheek", "TasksLibraryCategory": "Bibliotheek",
"TasksMaintenanceCategory": "Onderhoud", "TasksMaintenanceCategory": "Onderhoud",
"TaskCleanActivityLogDescription": "Verwijder activiteiten logs ouder dan de ingestelde tijd.", "TaskCleanActivityLogDescription": "Verwijder activiteiten logs ouder dan de ingestelde tijd.",
"TaskCleanActivityLog": "Leeg activiteiten logboek" "TaskCleanActivityLog": "Leeg activiteiten logboek",
"Undefined": "Niet gedefinieerd",
"Forced": "Geforceerd",
"Default": "Standaard"
} }

View File

@ -113,5 +113,10 @@
"TasksChannelsCategory": "Canais da Internet", "TasksChannelsCategory": "Canais da Internet",
"TasksApplicationCategory": "Aplicação", "TasksApplicationCategory": "Aplicação",
"TasksLibraryCategory": "Biblioteca", "TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção" "TasksMaintenanceCategory": "Manutenção",
"TaskCleanActivityLogDescription": "Apaga as entradas do registo de atividade anteriores à data configurada.",
"TaskCleanActivityLog": "Limpar registo de atividade",
"Undefined": "Indefinido",
"Forced": "Forçado",
"Default": "Padrão"
} }

View File

@ -112,5 +112,10 @@
"TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.", "TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.",
"TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.", "TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.",
"TaskRefreshPeople": "Atualizar pessoas", "TaskRefreshPeople": "Atualizar pessoas",
"TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados." "TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados.",
"TaskCleanActivityLog": "Limpar registo de atividade",
"Undefined": "Indefinido",
"Forced": "Forçado",
"Default": "Predefinição",
"TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado."
} }

View File

@ -114,5 +114,8 @@
"TasksLibraryCategory": "Librărie", "TasksLibraryCategory": "Librărie",
"TasksMaintenanceCategory": "Mentenanță", "TasksMaintenanceCategory": "Mentenanță",
"TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.", "TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.",
"TaskCleanActivityLog": "Curăță Jurnalul de Activitate" "TaskCleanActivityLog": "Curăță Jurnalul de Activitate",
"Undefined": "Nedefinit",
"Forced": "Forțat",
"Default": "Implicit"
} }

View File

@ -115,5 +115,8 @@
"TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.", "TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
"TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе.", "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе.",
"TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.", "TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.",
"TaskCleanActivityLog": "Очистить журнал активности" "TaskCleanActivityLog": "Очистить журнал активности",
"Undefined": "Не определено",
"Forced": "Форсир-ые",
"Default": "По умолчанию"
} }

View File

@ -2,7 +2,7 @@
"Albums": "Albumy", "Albums": "Albumy",
"AppDeviceValues": "Aplikácia: {0}, Zariadenie: {1}", "AppDeviceValues": "Aplikácia: {0}, Zariadenie: {1}",
"Application": "Aplikácia", "Application": "Aplikácia",
"Artists": "Umelci", "Artists": "Interpreti",
"AuthenticationSucceededWithUserName": "{0} úspešne overený", "AuthenticationSucceededWithUserName": "{0} úspešne overený",
"Books": "Knihy", "Books": "Knihy",
"CameraImageUploadedFrom": "Z {0} bola nahraná nová fotografia", "CameraImageUploadedFrom": "Z {0} bola nahraná nová fotografia",
@ -15,13 +15,13 @@
"Favorites": "Obľúbené", "Favorites": "Obľúbené",
"Folders": "Priečinky", "Folders": "Priečinky",
"Genres": "Žánre", "Genres": "Žánre",
"HeaderAlbumArtists": "Umelci albumu", "HeaderAlbumArtists": "Interpreti albumu",
"HeaderContinueWatching": "Pokračovať v pozeraní", "HeaderContinueWatching": "Pokračovať v pozeraní",
"HeaderFavoriteAlbums": "Obľúbené albumy", "HeaderFavoriteAlbums": "Obľúbené albumy",
"HeaderFavoriteArtists": "Obľúbení umelci", "HeaderFavoriteArtists": "Obľúbení interpreti",
"HeaderFavoriteEpisodes": "Obľúbené epizódy", "HeaderFavoriteEpisodes": "Obľúbené epizódy",
"HeaderFavoriteShows": "Obľúbené seriály", "HeaderFavoriteShows": "Obľúbené seriály",
"HeaderFavoriteSongs": "Obľúbené piesne", "HeaderFavoriteSongs": "Obľúbené skladby",
"HeaderLiveTV": "Živá TV", "HeaderLiveTV": "Živá TV",
"HeaderNextUp": "Nasleduje", "HeaderNextUp": "Nasleduje",
"HeaderRecordingGroups": "Skupiny nahrávok", "HeaderRecordingGroups": "Skupiny nahrávok",
@ -33,13 +33,13 @@
"LabelRunningTimeValue": "Dĺžka: {0}", "LabelRunningTimeValue": "Dĺžka: {0}",
"Latest": "Najnovšie", "Latest": "Najnovšie",
"MessageApplicationUpdated": "Jellyfin Server bol aktualizovaný", "MessageApplicationUpdated": "Jellyfin Server bol aktualizovaný",
"MessageApplicationUpdatedTo": "Jellyfin Server bol aktualizový na verziu {0}", "MessageApplicationUpdatedTo": "Jellyfin Server bol aktualizovaný na verziu {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Sekcia {0} konfigurácie servera bola aktualizovaná", "MessageNamedServerConfigurationUpdatedWithValue": "Sekcia {0} konfigurácie servera bola aktualizovaná",
"MessageServerConfigurationUpdated": "Konfigurácia servera bola aktualizovaná", "MessageServerConfigurationUpdated": "Konfigurácia servera bola aktualizovaná",
"MixedContent": "Zmiešaný obsah", "MixedContent": "Zmiešaný obsah",
"Movies": "Filmy", "Movies": "Filmy",
"Music": "Hudba", "Music": "Hudba",
"MusicVideos": "Hudobné videá", "MusicVideos": "Hudobné videoklipy",
"NameInstallFailed": "Inštalácia {0} zlyhala", "NameInstallFailed": "Inštalácia {0} zlyhala",
"NameSeasonNumber": "Séria {0}", "NameSeasonNumber": "Séria {0}",
"NameSeasonUnknown": "Neznáma séria", "NameSeasonUnknown": "Neznáma séria",
@ -71,7 +71,7 @@
"ScheduledTaskStartedWithName": "{0} zahájených", "ScheduledTaskStartedWithName": "{0} zahájených",
"ServerNameNeedsToBeRestarted": "{0} vyžaduje reštart", "ServerNameNeedsToBeRestarted": "{0} vyžaduje reštart",
"Shows": "Seriály", "Shows": "Seriály",
"Songs": "Piesne", "Songs": "Skladby",
"StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Prosím, skúste to o chvíľu znova.", "StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Prosím, skúste to o chvíľu znova.",
"SubtitleDownloadFailureForItem": "Sťahovanie titulkov pre {0} zlyhalo", "SubtitleDownloadFailureForItem": "Sťahovanie titulkov pre {0} zlyhalo",
"SubtitleDownloadFailureFromForItem": "Sťahovanie titulkov z {0} pre {1} zlyhalo", "SubtitleDownloadFailureFromForItem": "Sťahovanie titulkov z {0} pre {1} zlyhalo",
@ -89,29 +89,34 @@
"UserPolicyUpdatedWithName": "Používateľské zásady pre {0} boli aktualizované", "UserPolicyUpdatedWithName": "Používateľské zásady pre {0} boli aktualizované",
"UserStartedPlayingItemWithValues": "{0} spustil prehrávanie {1} na {2}", "UserStartedPlayingItemWithValues": "{0} spustil prehrávanie {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} ukončil prehrávanie {1} na {2}", "UserStoppedPlayingItemWithValues": "{0} ukončil prehrávanie {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} bol pridané do vašej knižnice médií", "ValueHasBeenAddedToLibrary": "{0} bol pridaný do vašej knižnice médií",
"ValueSpecialEpisodeName": "Špeciál - {0}", "ValueSpecialEpisodeName": "Špeciál - {0}",
"VersionNumber": "Verzia {0}", "VersionNumber": "Verzia {0}",
"TaskDownloadMissingSubtitlesDescription": "Vyhľadá na internete chýbajúce titulky podľa toho, ako sú nakonfigurované metadáta.", "TaskDownloadMissingSubtitlesDescription": "Vyhľadá na internete chýbajúce titulky podľa toho, ako sú nakonfigurované metadáta.",
"TaskDownloadMissingSubtitles": "Stiahnuť chýbajúce titulky", "TaskDownloadMissingSubtitles": "Stiahnuť chýbajúce titulky",
"TaskRefreshChannelsDescription": "Obnoví informácie o internetových kanáloch.", "TaskRefreshChannelsDescription": "Obnoví informácie o internetových kanáloch.",
"TaskRefreshChannels": "Obnoviť kanály", "TaskRefreshChannels": "Obnoviť kanály",
"TaskCleanTranscodeDescription": "Vymaže súbory transkódovania, ktoré sú staršie ako jeden deň.", "TaskCleanTranscodeDescription": "Vymaže prekódované súbory, ktoré sú staršie ako jeden deň.",
"TaskCleanTranscode": "Vyčistiť priečinok pre transkódovanie", "TaskCleanTranscode": "Vyčistiť priečinok pre prekódovanie",
"TaskUpdatePluginsDescription": "Stiahne a nainštaluje aktualizácie pre zásuvné moduly, ktoré sú nastavené tak, aby sa aktualizovali automaticky.", "TaskUpdatePluginsDescription": "Stiahne a nainštaluje aktualizácie pre zásuvné moduly, ktoré sú nastavené tak, aby sa aktualizovali automaticky.",
"TaskUpdatePlugins": "Aktualizovať zásuvné moduly", "TaskUpdatePlugins": "Aktualizovať zásuvné moduly",
"TaskRefreshPeopleDescription": "Aktualizuje metadáta pre hercov a režisérov vo vašej mediálnej knižnici.", "TaskRefreshPeopleDescription": "Aktualizuje metadáta pre hercov a režisérov vo vašej mediálnej knižnici.",
"TaskRefreshPeople": "Obnoviť osoby", "TaskRefreshPeople": "Obnoviť osoby",
"TaskCleanLogsDescription": "Vymaže log súbory, ktoré su staršie ako {0} deň/dni/dní.", "TaskCleanLogsDescription": "Vymaže log súbory, ktoré sú staršie ako {0} deň/dni/dní.",
"TaskCleanLogs": "Vyčistiť priečinok s logmi", "TaskCleanLogs": "Vyčistiť priečinok s logmi",
"TaskRefreshLibraryDescription": "Hľadá vo vašej mediálnej knižnici nové súbory a obnovuje metadáta.", "TaskRefreshLibraryDescription": "Hľadá vo vašej mediálnej knižnici nové súbory a obnovuje metadáta.",
"TaskRefreshLibrary": "Prehľadávať knižnicu medií", "TaskRefreshLibrary": "Prehľadávať knižnicu medií",
"TaskRefreshChapterImagesDescription": "Vytvorí náhľady pre videá, ktoré majú kapitoly.", "TaskRefreshChapterImagesDescription": "Vytvorí náhľady pre videá, ktoré majú kapitoly.",
"TaskRefreshChapterImages": "Extrahovať obrázky kapitol", "TaskRefreshChapterImages": "Extrahovať obrázky kapitol",
"TaskCleanCacheDescription": "Vymaže cache súbory, ktoré nie sú potrebné pre systém.", "TaskCleanCacheDescription": "Vymaže súbory vyrovnávacej pamäte, ktoré nie sú potrebné pre systém.",
"TaskCleanCache": "Vyčistiť Cache priečinok", "TaskCleanCache": "Vyčistiť priečinok vyrovnávacej pamäte",
"TasksChannelsCategory": "Internetové kanály", "TasksChannelsCategory": "Internetové kanály",
"TasksApplicationCategory": "Aplikácia", "TasksApplicationCategory": "Aplikácia",
"TasksLibraryCategory": "Knižnica", "TasksLibraryCategory": "Knižnica",
"TasksMaintenanceCategory": "Údržba" "TasksMaintenanceCategory": "Údržba",
"TaskCleanActivityLogDescription": "Vymaže záznamy aktivít v logu, ktoré sú staršie ako zadaná doba.",
"TaskCleanActivityLog": "Vyčistiť log aktivít",
"Undefined": "Nedefinované",
"Forced": "Vynútené",
"Default": "Predvolené"
} }

View File

@ -21,7 +21,7 @@
"Inherit": "மரபுரிமையாகப் பெறு", "Inherit": "மரபுரிமையாகப் பெறு",
"HeaderRecordingGroups": "பதிவு குழுக்கள்", "HeaderRecordingGroups": "பதிவு குழுக்கள்",
"Folders": "கோப்புறைகள்", "Folders": "கோப்புறைகள்",
"FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது", "FailedLoginAttemptWithUserName": "{0} இல் இருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
"DeviceOnlineWithName": "{0} இணைக்கப்பட்டது", "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
"DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது", "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
"Collections": "தொகுப்புகள்", "Collections": "தொகுப்புகள்",
@ -99,7 +99,7 @@
"MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0} புதுப்பிக்கப்பட்டது", "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0} புதுப்பிக்கப்பட்டது",
"TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.", "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.",
"UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது", "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன", "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இல் இருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
"TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.", "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
"TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.", "TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.",
"TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.", "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",

View File

@ -12,7 +12,7 @@
"DeviceOfflineWithName": "{0} bağlantısı kesildi", "DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı", "DeviceOnlineWithName": "{0} bağlı",
"FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu", "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
"Favorites": "Favoriler", "Favorites": "Favorilerim",
"Folders": "Klasörler", "Folders": "Klasörler",
"Genres": "Türler", "Genres": "Türler",
"HeaderAlbumArtists": "Albüm Sanatçıları", "HeaderAlbumArtists": "Albüm Sanatçıları",
@ -115,5 +115,7 @@
"TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar", "TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.", "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
"TaskCleanActivityLog": "İşlem Günlüğünü Temizle", "TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
"TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi." "TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi.",
"Undefined": "Bilinmeyen",
"Default": "Varsayılan"
} }

View File

@ -27,7 +27,7 @@
"Channels": "Канали", "Channels": "Канали",
"CameraImageUploadedFrom": "Нова фотографія завантажена з {0}", "CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
"Books": "Книги", "Books": "Книги",
"AuthenticationSucceededWithUserName": "{0} успішно авторизований", "AuthenticationSucceededWithUserName": "{0} успішно автентифіковано",
"Artists": "Виконавці", "Artists": "Виконавці",
"Application": "Додаток", "Application": "Додаток",
"AppDeviceValues": "Додаток: {0}, Пристрій: {1}", "AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
@ -112,5 +112,10 @@
"MessageServerConfigurationUpdated": "Конфігурація сервера оновлена", "MessageServerConfigurationUpdated": "Конфігурація сервера оновлена",
"MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено", "MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено",
"Inherit": "Успадкувати", "Inherit": "Успадкувати",
"HeaderRecordingGroups": "Групи запису" "HeaderRecordingGroups": "Групи запису",
"Forced": "Примусово",
"TaskCleanActivityLogDescription": "Видаляє старші за встановлений термін записи з журналу активності.",
"TaskCleanActivityLog": "Очистити журнал активності",
"Undefined": "Не визначено",
"Default": "За замовчуванням"
} }

View File

@ -16,7 +16,7 @@
"Albums": "Albums", "Albums": "Albums",
"Artists": "Các Nghệ Sĩ", "Artists": "Các Nghệ Sĩ",
"TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.", "TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
"TaskDownloadMissingSubtitles": "Tải xuống phụ đề bị thiếu", "TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",
"TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.", "TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
"TaskRefreshChannels": "Làm Mới Kênh", "TaskRefreshChannels": "Làm Mới Kênh",
"TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.", "TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.",
@ -24,11 +24,11 @@
"TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.", "TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.",
"TaskUpdatePlugins": "Cập Nhật Plugins", "TaskUpdatePlugins": "Cập Nhật Plugins",
"TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.", "TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.",
"TaskRefreshPeople": "Làm mới Người dùng", "TaskRefreshPeople": "Làm Mới Người Dùng",
"TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.", "TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.",
"TaskCleanLogs": "Làm sạch nhật ký", "TaskCleanLogs": "Làm Sạch Thư Mục Nhật Ký",
"TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm các tệp mới và làm mới thông tin chi tiết.", "TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm tệp mới và làm mới dữ liệu mô tả.",
"TaskRefreshLibrary": "Quét Thư viện Phương tiện", "TaskRefreshLibrary": "Quét Thư Viện Phương Tiện",
"TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.", "TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
"TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh", "TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
"TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.", "TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
@ -80,7 +80,7 @@
"NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có", "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có",
"NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.", "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.",
"NameSeasonUnknown": "Không Rõ Mùa", "NameSeasonUnknown": "Không Rõ Mùa",
"NameSeasonNumber": "Mùa {0}", "NameSeasonNumber": "Phần {0}",
"NameInstallFailed": "{0} cài đặt thất bại", "NameInstallFailed": "{0} cài đặt thất bại",
"MusicVideos": "Video Nhạc", "MusicVideos": "Video Nhạc",
"Music": "Nhạc", "Music": "Nhạc",

View File

@ -114,5 +114,8 @@
"TasksApplicationCategory": "應用程式", "TasksApplicationCategory": "應用程式",
"TasksMaintenanceCategory": "維護", "TasksMaintenanceCategory": "維護",
"TaskCleanActivityLogDescription": "刪除超過所設時間的活動紀錄。", "TaskCleanActivityLogDescription": "刪除超過所設時間的活動紀錄。",
"TaskCleanActivityLog": "清除活動紀錄" "TaskCleanActivityLog": "清除活動紀錄",
"Undefined": "未定義的",
"Forced": "強制",
"Default": "原本"
} }

View File

@ -557,6 +557,12 @@
"ThreeLetterISORegionName": "OMN", "ThreeLetterISORegionName": "OMN",
"TwoLetterISORegionName": "OM" "TwoLetterISORegionName": "OM"
}, },
{
"DisplayName": "Palestine",
"Name": "PS",
"ThreeLetterISORegionName": "PSE",
"TwoLetterISORegionName": "PS"
},
{ {
"DisplayName": "Panama", "DisplayName": "Panama",
"Name": "PA", "Name": "PA",

View File

@ -12,6 +12,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
@ -81,12 +82,7 @@ namespace Emby.Server.Implementations.MediaEncoder
return false; return false;
} }
if (video.VideoType == VideoType.Iso) if (video.VideoType == VideoType.Dvd)
{
return false;
}
if (video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd)
{ {
return false; return false;
} }
@ -140,15 +136,19 @@ namespace Emby.Server.Implementations.MediaEncoder
// Add some time for the first chapter to make sure we don't end up with a black image // Add some time for the first chapter to make sure we don't end up with a black image
var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks);
var protocol = MediaProtocol.File; var inputPath = video.Path;
var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, null, Array.Empty<string>());
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path));
var container = video.Container; var container = video.Container;
var mediaSource = new MediaSourceInfo
{
VideoType = video.VideoType,
IsoType = video.IsoType,
Protocol = video.PathProtocol.Value,
};
var tempFile = await _encoder.ExtractVideoImage(inputPath, container, protocol, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false);
File.Copy(tempFile, path, true); File.Copy(tempFile, path, true);
try try

View File

@ -1,556 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Networking
{
/// <summary>
/// Class to take care of network interface management.
/// </summary>
public class NetworkManager : INetworkManager
{
private readonly ILogger<NetworkManager> _logger;
private IPAddress[] _localIpAddresses;
private readonly object _localIpAddressSyncLock = new object();
private readonly object _subnetLookupLock = new object();
private readonly Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal);
private List<PhysicalAddress> _macAddresses;
/// <summary>
/// Initializes a new instance of the <see cref="NetworkManager"/> class.
/// </summary>
/// <param name="logger">Logger to use for messages.</param>
public NetworkManager(ILogger<NetworkManager> logger)
{
_logger = logger;
NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
}
/// <inheritdoc/>
public event EventHandler NetworkChanged;
/// <inheritdoc/>
public Func<string[]> LocalSubnetsFn { get; set; }
private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
_logger.LogDebug("NetworkAvailabilityChanged");
OnNetworkChanged();
}
private void OnNetworkAddressChanged(object sender, EventArgs e)
{
_logger.LogDebug("NetworkAddressChanged");
OnNetworkChanged();
}
private void OnNetworkChanged()
{
lock (_localIpAddressSyncLock)
{
_localIpAddresses = null;
_macAddresses = null;
}
NetworkChanged?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc/>
public IPAddress[] GetLocalIpAddresses()
{
lock (_localIpAddressSyncLock)
{
if (_localIpAddresses == null)
{
var addresses = GetLocalIpAddressesInternal().ToArray();
_localIpAddresses = addresses;
}
return _localIpAddresses;
}
}
private List<IPAddress> GetLocalIpAddressesInternal()
{
var list = GetIPsDefault().ToList();
if (list.Count == 0)
{
list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList();
}
var listClone = new List<IPAddress>();
var subnets = LocalSubnetsFn();
foreach (var i in list)
{
if (i.IsIPv6LinkLocal || i.ToString().StartsWith("169.254.", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (Array.IndexOf(subnets, $"[{i}]") == -1)
{
listClone.Add(i);
}
}
return listClone
.OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1)
// .ThenBy(i => listClone.IndexOf(i))
.GroupBy(i => i.ToString())
.Select(x => x.First())
.ToList();
}
/// <inheritdoc/>
public bool IsInPrivateAddressSpace(string endpoint)
{
return IsInPrivateAddressSpace(endpoint, true);
}
// Checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address
private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets)
{
if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase))
{
return true;
}
// IPV6
if (endpoint.Split('.').Length > 4)
{
// Handle ipv4 mapped to ipv6
var originalEndpoint = endpoint;
endpoint = endpoint.Replace("::ffff:", string.Empty, StringComparison.OrdinalIgnoreCase);
if (string.Equals(endpoint, originalEndpoint, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
// Private address space:
if (string.Equals(endpoint, "localhost", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (!IPAddress.TryParse(endpoint, out var ipAddress))
{
return false;
}
byte[] octet = ipAddress.GetAddressBytes();
if ((octet[0] == 10) ||
(octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
(octet[0] == 192 && octet[1] == 168) || // RFC1918
(octet[0] == 127) || // RFC1122
(octet[0] == 169 && octet[1] == 254)) // RFC3927
{
return true;
}
if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
{
return true;
}
return false;
}
/// <inheritdoc/>
public bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint)
{
if (endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase))
{
var endpointFirstPart = endpoint.Split('.')[0];
var subnets = GetSubnets(endpointFirstPart);
foreach (var subnet_Match in subnets)
{
// logger.LogDebug("subnet_Match:" + subnet_Match);
if (endpoint.StartsWith(subnet_Match + ".", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
// Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart
private List<string> GetSubnets(string endpointFirstPart)
{
lock (_subnetLookupLock)
{
if (_subnetLookup.TryGetValue(endpointFirstPart, out var subnets))
{
return subnets;
}
subnets = new List<string>();
foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces())
{
foreach (var unicastIPAddressInformation in adapter.GetIPProperties().UnicastAddresses)
{
if (unicastIPAddressInformation.Address.AddressFamily == AddressFamily.InterNetwork && endpointFirstPart == unicastIPAddressInformation.Address.ToString().Split('.')[0])
{
int subnet_Test = 0;
foreach (string part in unicastIPAddressInformation.IPv4Mask.ToString().Split('.'))
{
if (part.Equals("0", StringComparison.Ordinal))
{
break;
}
subnet_Test++;
}
var subnet_Match = string.Join(".", unicastIPAddressInformation.Address.ToString().Split('.').Take(subnet_Test).ToArray());
// TODO: Is this check necessary?
if (adapter.OperationalStatus == OperationalStatus.Up)
{
subnets.Add(subnet_Match);
}
}
}
}
_subnetLookup[endpointFirstPart] = subnets;
return subnets;
}
}
/// <inheritdoc/>
public bool IsInLocalNetwork(string endpoint)
{
return IsInLocalNetworkInternal(endpoint, true);
}
/// <inheritdoc/>
public bool IsAddressInSubnets(string addressString, string[] subnets)
{
return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets);
}
/// <inheritdoc/>
public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC)
{
byte[] octet = address.GetAddressBytes();
if ((octet[0] == 127) || // RFC1122
(octet[0] == 169 && octet[1] == 254)) // RFC3927
{
// don't use on loopback or 169 interfaces
return false;
}
string addressString = address.ToString();
string excludeAddress = "[" + addressString + "]";
var subnets = LocalSubnetsFn();
// Include any address if LAN subnets aren't specified
if (subnets.Length == 0)
{
return true;
}
// Exclude any addresses if they appear in the LAN list in [ ]
if (Array.IndexOf(subnets, excludeAddress) != -1)
{
return false;
}
return IsAddressInSubnets(address, addressString, subnets);
}
/// <summary>
/// Checks if the give address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format.
/// </summary>
/// <param name="address">IPAddress version of the address.</param>
/// <param name="addressString">The address to check.</param>
/// <param name="subnets">If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address.</param>
/// <returns><c>false</c>if the address isn't in the subnets, <c>true</c> otherwise.</returns>
private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets)
{
foreach (var subnet in subnets)
{
var normalizedSubnet = subnet.Trim();
// Is the subnet a host address and does it match the address being passes?
if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Parse CIDR subnets and see if address falls within it.
if (normalizedSubnet.Contains('/', StringComparison.Ordinal))
{
try
{
var ipNetwork = IPNetwork.Parse(normalizedSubnet);
if (ipNetwork.Contains(address))
{
return true;
}
}
catch
{
// Ignoring - invalid subnet passed encountered.
}
}
}
return false;
}
private bool IsInLocalNetworkInternal(string endpoint, bool resolveHost)
{
if (string.IsNullOrEmpty(endpoint))
{
throw new ArgumentNullException(nameof(endpoint));
}
if (IPAddress.TryParse(endpoint, out var address))
{
var addressString = address.ToString();
var localSubnetsFn = LocalSubnetsFn;
if (localSubnetsFn != null)
{
var localSubnets = localSubnetsFn();
foreach (var subnet in localSubnets)
{
// Only validate if there's at least one valid entry.
if (!string.IsNullOrWhiteSpace(subnet))
{
return IsAddressInSubnets(address, addressString, localSubnets) || IsInPrivateAddressSpace(addressString, false);
}
}
}
int lengthMatch = 100;
if (address.AddressFamily == AddressFamily.InterNetwork)
{
lengthMatch = 4;
if (IsInPrivateAddressSpace(addressString, true))
{
return true;
}
}
else if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
lengthMatch = 9;
if (IsInPrivateAddressSpace(endpoint, true))
{
return true;
}
}
// Should be even be doing this with ipv6?
if (addressString.Length >= lengthMatch)
{
var prefix = addressString.Substring(0, lengthMatch);
if (GetLocalIpAddresses().Any(i => i.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
}
else if (resolveHost)
{
if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out var uri))
{
try
{
var host = uri.DnsSafeHost;
_logger.LogDebug("Resolving host {0}", host);
address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault();
if (address != null)
{
_logger.LogDebug("{0} resolved to {1}", host, address);
return IsInLocalNetworkInternal(address.ToString(), false);
}
}
catch (InvalidOperationException)
{
// Can happen with reverse proxy or IIS url rewriting?
}
catch (Exception ex)
{
_logger.LogError(ex, "Error resolving hostname");
}
}
}
return false;
}
private static Task<IPAddress[]> GetIpAddresses(string hostName)
{
return Dns.GetHostAddressesAsync(hostName);
}
private IEnumerable<IPAddress> GetIPsDefault()
{
IEnumerable<NetworkInterface> interfaces;
try
{
interfaces = NetworkInterface.GetAllNetworkInterfaces()
.Where(x => x.OperationalStatus == OperationalStatus.Up
|| x.OperationalStatus == OperationalStatus.Unknown);
}
catch (NetworkInformationException ex)
{
_logger.LogError(ex, "Error in GetAllNetworkInterfaces");
return Enumerable.Empty<IPAddress>();
}
return interfaces.SelectMany(network =>
{
var ipProperties = network.GetIPProperties();
// Exclude any addresses if they appear in the LAN list in [ ]
return ipProperties.UnicastAddresses
.Select(i => i.Address)
.Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6);
}).GroupBy(i => i.ToString())
.Select(x => x.First());
}
private static async Task<IEnumerable<IPAddress>> GetLocalIpAddressesFallback()
{
var host = await Dns.GetHostEntryAsync(Dns.GetHostName()).ConfigureAwait(false);
// Reverse them because the last one is usually the correct one
// It's not fool-proof so ultimately the consumer will have to examine them and decide
return host.AddressList
.Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6)
.Reverse();
}
/// <summary>
/// Gets a random port number that is currently available.
/// </summary>
/// <returns>System.Int32.</returns>
public int GetRandomUnusedTcpPort()
{
var listener = new TcpListener(IPAddress.Any, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
/// <inheritdoc/>
public int GetRandomUnusedUdpPort()
{
var localEndPoint = new IPEndPoint(IPAddress.Any, 0);
using (var udpClient = new UdpClient(localEndPoint))
{
return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
}
}
/// <inheritdoc/>
public List<PhysicalAddress> GetMacAddresses()
{
return _macAddresses ??= GetMacAddressesInternal().ToList();
}
private static IEnumerable<PhysicalAddress> GetMacAddressesInternal()
=> NetworkInterface.GetAllNetworkInterfaces()
.Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback)
.Select(x => x.GetPhysicalAddress())
.Where(x => !x.Equals(PhysicalAddress.None));
/// <inheritdoc/>
public bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask)
{
IPAddress network1 = GetNetworkAddress(address1, subnetMask);
IPAddress network2 = GetNetworkAddress(address2, subnetMask);
return network1.Equals(network2);
}
private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask)
{
byte[] ipAdressBytes = address.GetAddressBytes();
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
if (ipAdressBytes.Length != subnetMaskBytes.Length)
{
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
}
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
for (int i = 0; i < broadcastAddress.Length; i++)
{
broadcastAddress[i] = (byte)(ipAdressBytes[i] & subnetMaskBytes[i]);
}
return new IPAddress(broadcastAddress);
}
/// <inheritdoc/>
public IPAddress GetLocalIpSubnetMask(IPAddress address)
{
NetworkInterface[] interfaces;
try
{
var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown };
interfaces = NetworkInterface.GetAllNetworkInterfaces()
.Where(i => validStatuses.Contains(i.OperationalStatus))
.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in GetAllNetworkInterfaces");
return null;
}
foreach (NetworkInterface ni in interfaces)
{
foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
{
if (ip.Address.Equals(address) && ip.IPv4Mask != null)
{
return ip.IPv4Mask;
}
}
}
return null;
}
}
}

View File

@ -150,7 +150,7 @@ namespace Emby.Server.Implementations.Playlists
await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None) await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
.ConfigureAwait(false); .ConfigureAwait(false);
if (options.ItemIdList.Length > 0) if (options.ItemIdList.Count > 0)
{ {
await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false) await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false)
{ {
@ -184,7 +184,7 @@ namespace Emby.Server.Implementations.Playlists
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
} }
public Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId) public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
{ {
var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId); var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Playlists
}); });
} }
private async Task AddToPlaylistInternal(Guid playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options) private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options)
{ {
// Retrieve the existing playlist // Retrieve the existing playlist
var playlist = _libraryManager.GetItemById(playlistId) as Playlist var playlist = _libraryManager.GetItemById(playlistId) as Playlist

View File

@ -243,7 +243,7 @@ namespace Emby.Server.Implementations.QuickConnect
Span<byte> bytes = stackalloc byte[length]; Span<byte> bytes = stackalloc byte[length];
_rng.GetBytes(bytes); _rng.GetBytes(bytes);
return Hex.Encode(bytes); return Convert.ToHexString(bytes);
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@ -1,45 +0,0 @@
#pragma warning disable CS1591
using System;
using System.IO;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations
{
public class ResourceFileManager : IResourceFileManager
{
private readonly IFileSystem _fileSystem;
private readonly ILogger<ResourceFileManager> _logger;
public ResourceFileManager(ILogger<ResourceFileManager> logger, IFileSystem fileSystem)
{
_logger = logger;
_fileSystem = fileSystem;
}
public string GetResourcePath(string basePath, string virtualPath)
{
var fullPath = Path.Combine(basePath, virtualPath.Replace('/', Path.DirectorySeparatorChar));
try
{
fullPath = Path.GetFullPath(fullPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving full path");
}
// Don't allow file system access outside of the source folder
if (!_fileSystem.ContainsSubPath(basePath, fullPath))
{
throw new SecurityException("Access denied");
}
return fullPath;
}
}
}

View File

@ -5,10 +5,10 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{ {
@ -23,8 +23,12 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DeleteTranscodeFileTask" /> class. /// Initializes a new instance of the <see cref="DeleteTranscodeFileTask"/> class.
/// </summary> /// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{DeleteTranscodeFileTask}"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
public DeleteTranscodeFileTask( public DeleteTranscodeFileTask(
ILogger<DeleteTranscodeFileTask> logger, ILogger<DeleteTranscodeFileTask> logger,
IFileSystem fileSystem, IFileSystem fileSystem,
@ -37,11 +41,42 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
_localization = localization; _localization = localization;
} }
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskCleanTranscode");
/// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
/// <inheritdoc />
public string Key => "DeleteTranscodeFiles";
/// <inheritdoc />
public bool IsHidden => false;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
/// <summary> /// <summary>
/// Creates the triggers that define when the task will run. /// Creates the triggers that define when the task will run.
/// </summary> /// </summary>
/// <returns>IEnumerable{BaseTaskTrigger}.</returns> /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => new List<TaskTriggerInfo>(); public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
{
new TaskTriggerInfo
{
Type = TaskTriggerInfo.TriggerInterval,
IntervalTicks = TimeSpan.FromHours(24).Ticks
}
};
}
/// <summary> /// <summary>
/// Returns the task to be executed. /// Returns the task to be executed.
@ -131,26 +166,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
_logger.LogError(ex, "Error deleting file {path}", path); _logger.LogError(ex, "Error deleting file {path}", path);
} }
} }
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskCleanTranscode");
/// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
/// <inheritdoc />
public string Key => "DeleteTranscodeFiles";
/// <inheritdoc />
public bool IsHidden => false;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
} }
} }

View File

@ -58,8 +58,7 @@ namespace Emby.Server.Implementations.Session
/// <summary> /// <summary>
/// The active connections. /// The active connections.
/// </summary> /// </summary>
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new (StringComparer.OrdinalIgnoreCase);
new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer; private Timer _idleTimer;
@ -196,7 +195,7 @@ namespace Emby.Server.Implementations.Session
{ {
if (!string.IsNullOrEmpty(info.DeviceId)) if (!string.IsNullOrEmpty(info.DeviceId))
{ {
var capabilities = GetSavedCapabilities(info.DeviceId); var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
if (capabilities != null) if (capabilities != null)
{ {
@ -1677,27 +1676,10 @@ namespace Emby.Server.Implementations.Session
SessionInfo = session SessionInfo = session
}); });
try _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
{
SaveCapabilities(session.DeviceId, capabilities);
}
catch (Exception ex)
{
_logger.LogError("Error saving device capabilities", ex);
}
} }
} }
private ClientCapabilities GetSavedCapabilities(string deviceId)
{
return _deviceManager.GetCapabilities(deviceId);
}
private void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
{
_deviceManager.SaveCapabilities(deviceId, capabilities);
}
/// <summary> /// <summary>
/// Converts a BaseItem to a BaseItemInfo. /// Converts a BaseItem to a BaseItemInfo.
/// </summary> /// </summary>

View File

@ -56,13 +56,11 @@ namespace Emby.Server.Implementations.TV
return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, dtoOptions), request); return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, dtoOptions), request);
} }
var parentIdGuid = string.IsNullOrEmpty(request.ParentId) ? (Guid?)null : new Guid(request.ParentId);
BaseItem[] parents; BaseItem[] parents;
if (parentIdGuid.HasValue) if (request.ParentId.HasValue)
{ {
var parent = _libraryManager.GetItemById(parentIdGuid.Value); var parent = _libraryManager.GetItemById(request.ParentId.Value);
if (parent != null) if (parent != null)
{ {
@ -146,28 +144,10 @@ namespace Emby.Server.Implementations.TV
var allNextUp = seriesKeys var allNextUp = seriesKeys
.Select(i => GetNextUp(i, currentUser, dtoOptions)); .Select(i => GetNextUp(i, currentUser, dtoOptions));
// allNextUp = allNextUp.OrderByDescending(i => i.Item1);
// If viewing all next up for all series, remove first episodes
// But if that returns empty, keep those first episodes (avoid completely empty view)
var alwaysEnableFirstEpisode = !string.IsNullOrEmpty(request.SeriesId);
var anyFound = false;
return allNextUp return allNextUp
.Where(i => .Where(i =>
{ {
if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue) return i.Item1 != DateTime.MinValue;
{
anyFound = true;
return true;
}
if (!anyFound && i.Item1 == DateTime.MinValue)
{
return true;
}
return false;
}) })
.Select(i => i.Item2()) .Select(i => i.Item2())
.Where(i => i != null); .Where(i => i != null);
@ -210,7 +190,7 @@ namespace Emby.Server.Implementations.TV
Func<Episode> getEpisode = () => Func<Episode> getEpisode = () =>
{ {
return _libraryManager.GetItemList(new InternalItemsQuery(user) var nextEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user)
{ {
AncestorWithPresentationUniqueKey = null, AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey, SeriesPresentationUniqueKey = seriesKey,
@ -223,6 +203,18 @@ namespace Emby.Server.Implementations.TV
MinSortName = lastWatchedEpisode?.SortName, MinSortName = lastWatchedEpisode?.SortName,
DtoOptions = dtoOptions DtoOptions = dtoOptions
}).Cast<Episode>().FirstOrDefault(); }).Cast<Episode>().FirstOrDefault();
if (nextEpisode != null)
{
var userData = _userDataManager.GetUserData(user, nextEpisode);
if (userData.PlaybackPositionTicks > 0)
{
return null;
}
}
return nextEpisode;
}; };
if (lastWatchedEpisode != null) if (lastWatchedEpisode != null)

View File

@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Udp
{ {
string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey]) string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey])
? _config[AddressOverrideConfigKey] ? _config[AddressOverrideConfigKey]
: await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); : _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
if (!string.IsNullOrEmpty(localUrl)) if (!string.IsNullOrEmpty(localUrl))
{ {

View File

@ -6,13 +6,15 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Runtime.Serialization; using System.Net.Http.Json;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
@ -21,8 +23,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Updates; using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -40,9 +40,9 @@ namespace Emby.Server.Implementations.Updates
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IEventManager _eventManager; private readonly IEventManager _eventManager;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IJsonSerializer _jsonSerializer;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _jsonSerializerOptions;
/// <summary> /// <summary>
/// Gets the application host. /// Gets the application host.
@ -70,7 +70,6 @@ namespace Emby.Server.Implementations.Updates
IApplicationPaths appPaths, IApplicationPaths appPaths,
IEventManager eventManager, IEventManager eventManager,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IJsonSerializer jsonSerializer,
IServerConfigurationManager config, IServerConfigurationManager config,
IFileSystem fileSystem, IFileSystem fileSystem,
IZipClient zipClient) IZipClient zipClient)
@ -83,33 +82,43 @@ namespace Emby.Server.Implementations.Updates
_appPaths = appPaths; _appPaths = appPaths;
_eventManager = eventManager; _eventManager = eventManager;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_jsonSerializer = jsonSerializer;
_config = config; _config = config;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_zipClient = zipClient; _zipClient = zipClient;
_jsonSerializerOptions = JsonDefaults.GetOptions();
} }
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal; public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<PackageInfo>> GetPackages(string manifest, CancellationToken cancellationToken = default) public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default)
{ {
try try
{ {
using var response = await _httpClientFactory.CreateClient(NamedClient.Default) var packages = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(manifest, cancellationToken).ConfigureAwait(false); .GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); if (packages == null)
try
{ {
return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(stream).ConfigureAwait(false);
}
catch (SerializationException ex)
{
_logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
return Array.Empty<PackageInfo>(); return Array.Empty<PackageInfo>();
} }
// Store the repository and repository url with each version, as they may be spread apart.
foreach (var entry in packages)
{
foreach (var ver in entry.versions)
{
ver.repositoryName = manifestName;
ver.repositoryUrl = manifest;
}
}
return packages;
}
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
return Array.Empty<PackageInfo>();
} }
catch (UriFormatException ex) catch (UriFormatException ex)
{ {
@ -123,17 +132,75 @@ namespace Emby.Server.Implementations.Updates
} }
} }
private static void MergeSort(IList<VersionInfo> source, IList<VersionInfo> dest)
{
int sLength = source.Count - 1;
int dLength = dest.Count;
int s = 0, d = 0;
var sourceVersion = source[0].VersionNumber;
var destVersion = dest[0].VersionNumber;
while (d < dLength)
{
if (sourceVersion.CompareTo(destVersion) >= 0)
{
if (s < sLength)
{
sourceVersion = source[++s].VersionNumber;
}
else
{
// Append all of destination to the end of source.
while (d < dLength)
{
source.Add(dest[d++]);
}
break;
}
}
else
{
source.Insert(s++, dest[d++]);
if (d >= dLength)
{
break;
}
sLength++;
destVersion = dest[d].VersionNumber;
}
}
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default) public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
{ {
var result = new List<PackageInfo>(); var result = new List<PackageInfo>();
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories) foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
{ {
foreach (var package in await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true)) if (repository.Enabled)
{ {
package.repositoryName = repository.Name; // Where repositories have the same content, the details of the first is taken.
package.repositoryUrl = repository.Url; foreach (var package in await GetPackages(repository.Name, repository.Url, cancellationToken).ConfigureAwait(true))
result.Add(package); {
if (!Guid.TryParse(package.guid, out var packageGuid))
{
// Package doesn't have a valid GUID, skip.
continue;
}
var existing = FilterPackages(result, package.name, packageGuid).FirstOrDefault();
if (existing != null)
{
// Assumption is both lists are ordered, so slot these into the correct place.
MergeSort(existing.versions, package.versions);
}
else
{
result.Add(package);
}
}
} }
} }
@ -144,7 +211,8 @@ namespace Emby.Server.Implementations.Updates
public IEnumerable<PackageInfo> FilterPackages( public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages, IEnumerable<PackageInfo> availablePackages,
string name = null, string name = null,
Guid guid = default) Guid guid = default,
Version specificVersion = null)
{ {
if (name != null) if (name != null)
{ {
@ -156,6 +224,11 @@ namespace Emby.Server.Implementations.Updates
availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid); availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid);
} }
if (specificVersion != null)
{
availablePackages = availablePackages.Where(x => x.versions.Where(y => y.VersionNumber.Equals(specificVersion)).Any());
}
return availablePackages; return availablePackages;
} }
@ -167,7 +240,7 @@ namespace Emby.Server.Implementations.Updates
Version minVersion = null, Version minVersion = null,
Version specificVersion = null) Version specificVersion = null)
{ {
var package = FilterPackages(availablePackages, name, guid).FirstOrDefault(); var package = FilterPackages(availablePackages, name, guid, specificVersion).FirstOrDefault();
// Package not found in repository // Package not found in repository
if (package == null) if (package == null)
@ -181,21 +254,21 @@ namespace Emby.Server.Implementations.Updates
if (specificVersion != null) if (specificVersion != null)
{ {
availableVersions = availableVersions.Where(x => new Version(x.version) == specificVersion); availableVersions = availableVersions.Where(x => x.VersionNumber.Equals(specificVersion));
} }
else if (minVersion != null) else if (minVersion != null)
{ {
availableVersions = availableVersions.Where(x => new Version(x.version) >= minVersion); availableVersions = availableVersions.Where(x => x.VersionNumber >= minVersion);
} }
foreach (var v in availableVersions.OrderByDescending(x => x.version)) foreach (var v in availableVersions.OrderByDescending(x => x.VersionNumber))
{ {
yield return new InstallationInfo yield return new InstallationInfo
{ {
Changelog = v.changelog, Changelog = v.changelog,
Guid = new Guid(package.guid), Guid = new Guid(package.guid),
Name = package.name, Name = package.name,
Version = new Version(v.version), Version = v.VersionNumber,
SourceUrl = v.sourceUrl, SourceUrl = v.sourceUrl,
Checksum = v.checksum Checksum = v.checksum
}; };
@ -333,7 +406,7 @@ namespace Emby.Server.Implementations.Updates
string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name); string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default) using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(package.SourceUrl, cancellationToken).ConfigureAwait(false); .GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// CA5351: Do Not Use Broken Cryptographic Algorithms // CA5351: Do Not Use Broken Cryptographic Algorithms
@ -341,7 +414,7 @@ namespace Emby.Server.Implementations.Updates
using var md5 = MD5.Create(); using var md5 = MD5.Create();
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var hash = Hex.Encode(md5.ComputeHash(stream)); var hash = Convert.ToHexString(md5.ComputeHash(stream));
if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
{ {
_logger.LogError( _logger.LogError(

View File

@ -18,6 +18,7 @@ namespace Jellyfin.Api.Auth
public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{ {
private readonly IAuthService _authService; private readonly IAuthService _authService;
private readonly ILogger<CustomAuthenticationHandler> _logger;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CustomAuthenticationHandler" /> class. /// Initializes a new instance of the <see cref="CustomAuthenticationHandler" /> class.
@ -35,6 +36,7 @@ namespace Jellyfin.Api.Auth
ISystemClock clock) : base(options, logger, encoder, clock) ISystemClock clock) : base(options, logger, encoder, clock)
{ {
_authService = authService; _authService = authService;
_logger = logger.CreateLogger<CustomAuthenticationHandler>();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -70,7 +72,8 @@ namespace Jellyfin.Api.Auth
} }
catch (AuthenticationException ex) catch (AuthenticationException ex)
{ {
return Task.FromResult(AuthenticateResult.Fail(ex)); _logger.LogDebug(ex, "Error authenticating with {Handler}", nameof(CustomAuthenticationHandler));
return Task.FromResult(AuthenticateResult.NoResult());
} }
catch (SecurityException ex) catch (SecurityException ex)
{ {

View File

@ -1,4 +1,4 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;

View File

@ -1,9 +1,8 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
@ -87,26 +86,26 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] string? officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery] string? tags, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery] string? years, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? studios, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery] string? studioIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
@ -119,64 +118,55 @@ namespace Jellyfin.Api.Controllers
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null; User? user = null;
BaseItem parentItem; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
if (userId.HasValue && !userId.Equals(Guid.Empty)) if (userId.HasValue && !userId.Equals(Guid.Empty))
{ {
user = _userManager.GetUserById(userId.Value); user = _userManager.GetUserById(userId.Value);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
} }
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = excludeItemTypesArr, ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = includeItemTypesArr, IncludeItemTypes = includeItemTypes,
MediaTypes = mediaTypesArr, MediaTypes = mediaTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
NameLessThan = nameLessThan, NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith, NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater, NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, '|', true), Tags = tags,
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), OfficialRatings = officialRatings,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
GenreIds = RequestHelpers.GetGuids(genreIds), GenreIds = genreIds,
StudioIds = RequestHelpers.GetGuids(studioIds), StudioIds = studioIds,
Person = person, Person = person,
PersonIds = RequestHelpers.GetGuids(personIds), PersonIds = personIds,
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), Years = years,
MinCommunityRating = minCommunityRating, MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount EnableTotalRecordCount = enableTotalRecordCount
}; };
if (!string.IsNullOrWhiteSpace(parentId)) if (parentId.HasValue)
{ {
if (parentItem is Folder) if (parentItem is Folder)
{ {
query.AncestorIds = new[] { new Guid(parentId) }; query.AncestorIds = new[] { parentId.Value };
} }
else else
{ {
query.ItemIds = new[] { new Guid(parentId) }; query.ItemIds = new[] { parentId.Value };
} }
} }
// Studios // Studios
if (!string.IsNullOrEmpty(studios)) if (studios.Length != 0)
{ {
query.StudioIds = studios.Split('|').Select(i => query.StudioIds = studios.Select(i =>
{ {
try try
{ {
@ -230,7 +220,7 @@ namespace Jellyfin.Api.Controllers
var (baseItem, itemCounts) = i; var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes)) if (includeItemTypes.Length != 0)
{ {
dto.ChildCount = itemCounts.ItemCount; dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount; dto.ProgramCount = itemCounts.ProgramCount;
@ -295,26 +285,26 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] string? officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery] string? tags, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery] string? years, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? studios, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery] string? studioIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
@ -327,64 +317,55 @@ namespace Jellyfin.Api.Controllers
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null; User? user = null;
BaseItem parentItem; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
if (userId.HasValue && !userId.Equals(Guid.Empty)) if (userId.HasValue && !userId.Equals(Guid.Empty))
{ {
user = _userManager.GetUserById(userId.Value); user = _userManager.GetUserById(userId.Value);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
} }
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = excludeItemTypesArr, ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = includeItemTypesArr, IncludeItemTypes = includeItemTypes,
MediaTypes = mediaTypesArr, MediaTypes = mediaTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
NameLessThan = nameLessThan, NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith, NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater, NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, '|', true), Tags = tags,
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), OfficialRatings = officialRatings,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
GenreIds = RequestHelpers.GetGuids(genreIds), GenreIds = genreIds,
StudioIds = RequestHelpers.GetGuids(studioIds), StudioIds = studioIds,
Person = person, Person = person,
PersonIds = RequestHelpers.GetGuids(personIds), PersonIds = personIds,
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), Years = years,
MinCommunityRating = minCommunityRating, MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount EnableTotalRecordCount = enableTotalRecordCount
}; };
if (!string.IsNullOrWhiteSpace(parentId)) if (parentId.HasValue)
{ {
if (parentItem is Folder) if (parentItem is Folder)
{ {
query.AncestorIds = new[] { new Guid(parentId) }; query.AncestorIds = new[] { parentId.Value };
} }
else else
{ {
query.ItemIds = new[] { new Guid(parentId) }; query.ItemIds = new[] { parentId.Value };
} }
} }
// Studios // Studios
if (!string.IsNullOrEmpty(studios)) if (studios.Length != 0)
{ {
query.StudioIds = studios.Split('|').Select(i => query.StudioIds = studios.Select(i =>
{ {
try try
{ {
@ -438,7 +419,7 @@ namespace Jellyfin.Api.Controllers
var (baseItem, itemCounts) = i; var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes)) if (includeItemTypes.Length != 0)
{ {
dto.ChildCount = itemCounts.ItemCount; dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount; dto.ProgramCount = itemCounts.ProgramCount;

View File

@ -78,22 +78,20 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodingReasons">Optional. The transcoding reason.</param> /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param> /// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Audio stream returned.</response> /// <response code="200">Audio stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns> /// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("{itemId}/stream.{container:required}", Name = "GetAudioStreamByContainer")]
[HttpGet("{itemId}/stream", Name = "GetAudioStream")] [HttpGet("{itemId}/stream", Name = "GetAudioStream")]
[HttpHead("{itemId}/stream.{container:required}", Name = "HeadAudioStreamByContainer")]
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")] [HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesAudioFile] [ProducesAudioFile]
public async Task<ActionResult> GetAudioStream( public async Task<ActionResult> GetAudioStream(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromRoute] string? container, [FromQuery] string? container,
[FromQuery] bool? @static, [FromQuery] bool? @static,
[FromQuery] string? @params, [FromQuery] string? @params,
[FromQuery] string? tag, [FromQuery] string? tag,
@ -136,7 +134,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec, [FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec, [FromQuery] string? subtitleCodec,
[FromQuery] string? transcodingReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context, [FromQuery] EncodingContext? context,
@ -188,7 +186,172 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodingReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Static,
StreamOptions = streamOptions
};
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
}
/// <summary>
/// Gets an audio stream.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="container">The audio container.</param>
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
/// <param name="params">The streaming parameters.</param>
/// <param name="tag">The tag.</param>
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
/// <param name="maxRefFrames">Optional.</param>
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param>
/// <response code="200">Audio stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
[HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
[HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStreamByContainer(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
[FromQuery] long? startTimeTicks,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
[FromQuery] int? maxRefFrames,
[FromQuery] int? maxVideoBitDepth,
[FromQuery] bool? requireAvc,
[FromQuery] bool? deInterlace,
[FromQuery] bool? requireNonAnamorphic,
[FromQuery] int? transcodingMaxAudioChannels,
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string>? streamOptions)
{
StreamingRequestDto streamingRequest = new StreamingRequestDto
{
Id = itemId,
Container = container,
Static = @static ?? true,
Params = @params,
Tag = tag,
DeviceProfileId = deviceProfileId,
PlaySessionId = playSessionId,
SegmentContainer = segmentContainer,
SegmentLength = segmentLength,
MinSegments = minSegments,
MediaSourceId = mediaSourceId,
DeviceId = deviceId,
AudioCodec = audioCodec,
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
Level = level,
Framerate = framerate,
MaxFramerate = maxFramerate,
CopyTimestamps = copyTimestamps ?? true,
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod,
MaxRefFrames = maxRefFrames,
MaxVideoBitDepth = maxVideoBitDepth,
RequireAvc = requireAvc ?? true,
DeInterlace = deInterlace ?? true,
RequireNonAnamorphic = requireNonAnamorphic ?? true,
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
CpuCoreLimit = cpuCoreLimit,
LiveStreamId = liveStreamId,
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Static, Context = context ?? EncodingContext.Static,

View File

@ -1,4 +1,4 @@
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Branding; using MediaBrowser.Model.Branding;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;

View File

@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Channel features returned.</response> /// <response code="200">Channel features returned.</response>
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns> /// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
[HttpGet("{channelId}/Features")] [HttpGet("{channelId}/Features")]
public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] string channelId) public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] Guid channelId)
{ {
return _channelManager.GetChannelFeatures(channelId); return _channelManager.GetChannelFeatures(channelId);
} }
@ -198,7 +198,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? channelIds) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
@ -208,11 +208,7 @@ namespace Jellyfin.Api.Controllers
{ {
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
ChannelIds = (channelIds ?? string.Empty) ChannelIds = channelIds,
.Split(',')
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => new Guid(i))
.ToArray(),
DtoOptions = new DtoOptions { Fields = fields } DtoOptions = new DtoOptions { Fields = fields }
}; };

View File

@ -1,9 +1,10 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
@ -54,7 +55,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<CollectionCreationResult>> CreateCollection( public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
[FromQuery] string? name, [FromQuery] string? name,
[FromQuery] string? ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery] bool isLocked = false) [FromQuery] bool isLocked = false)
{ {
@ -65,7 +66,7 @@ namespace Jellyfin.Api.Controllers
IsLocked = isLocked, IsLocked = isLocked,
Name = name, Name = name,
ParentId = parentId, ParentId = parentId,
ItemIdList = RequestHelpers.Split(ids, ',', true), ItemIdList = ids,
UserIds = new[] { userId } UserIds = new[] { userId }
}).ConfigureAwait(false); }).ConfigureAwait(false);
@ -88,9 +89,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")] [HttpPost("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids) public async Task<ActionResult> AddToCollection(
[FromRoute, Required] Guid collectionId,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{ {
await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true); await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
return NoContent(); return NoContent();
} }
@ -103,9 +106,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")] [HttpDelete("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids) public async Task<ActionResult> RemoveFromCollection(
[FromRoute, Required] Guid collectionId,
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{ {
await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false); await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;

View File

@ -252,7 +252,7 @@ namespace Jellyfin.Api.Controllers
private string GetAbsoluteUri() private string GetAbsoluteUri()
{ {
return $"{Request.Scheme}://{Request.Host}{Request.Path}"; return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
} }
private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service) private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)

View File

@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodingReasons">Optional. The transcoding reason.</param> /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@ -216,7 +216,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec, [FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec, [FromQuery] string? subtitleCodec,
[FromQuery] string? transcodingReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext context,
@ -268,7 +268,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodingReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context,
@ -326,7 +326,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodingReasons">Optional. The transcoding reason.</param> /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@ -383,7 +383,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec, [FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec, [FromQuery] string? subtitleCodec,
[FromQuery] string? transcodingReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext context,
@ -435,7 +435,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodingReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context,
@ -492,7 +492,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodingReasons">Optional. The transcoding reason.</param> /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@ -546,7 +546,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec, [FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec, [FromQuery] string? subtitleCodec,
[FromQuery] string? transcodingReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext context,
@ -598,7 +598,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodingReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context,
@ -656,7 +656,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodingReasons">Optional. The transcoding reason.</param> /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@ -711,7 +711,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec, [FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec, [FromQuery] string? subtitleCodec,
[FromQuery] string? transcodingReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext context,
@ -763,7 +763,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodingReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context,
@ -823,7 +823,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodingReasons">Optional. The transcoding reason.</param> /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@ -838,7 +838,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId, [FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId, [FromRoute, Required] int segmentId,
[FromRoute] string container, [FromRoute, Required] string container,
[FromQuery] bool? @static, [FromQuery] bool? @static,
[FromQuery] string? @params, [FromQuery] string? @params,
[FromQuery] string? tag, [FromQuery] string? tag,
@ -881,7 +881,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec, [FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec, [FromQuery] string? subtitleCodec,
[FromQuery] string? transcodingReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext context,
@ -933,7 +933,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodingReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context,
@ -994,7 +994,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param> /// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param> /// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param> /// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
/// <param name="transcodingReasons">Optional. The transcoding reason.</param> /// <param name="transcodeReasons">Optional. The transcoding reason.</param>
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param> /// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param> /// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
@ -1009,7 +1009,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId, [FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId, [FromRoute, Required] int segmentId,
[FromRoute] string container, [FromRoute, Required] string container,
[FromQuery] bool? @static, [FromQuery] bool? @static,
[FromQuery] string? @params, [FromQuery] string? @params,
[FromQuery] string? tag, [FromQuery] string? tag,
@ -1053,7 +1053,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableMpegtsM2TsMode, [FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] string? videoCodec, [FromQuery] string? videoCodec,
[FromQuery] string? subtitleCodec, [FromQuery] string? subtitleCodec,
[FromQuery] string? transcodingReasons, [FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext context, [FromQuery] EncodingContext context,
@ -1105,7 +1105,7 @@ namespace Jellyfin.Api.Controllers
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
VideoCodec = videoCodec, VideoCodec = videoCodec,
SubtitleCodec = subtitleCodec, SubtitleCodec = subtitleCodec,
TranscodeReasons = transcodingReasons, TranscodeReasons = transcodeReasons,
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context, Context = context,

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
@ -49,37 +50,29 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy( public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? parentId, [FromQuery] Guid? parentId,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] string? mediaTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
{ {
var parentItem = string.IsNullOrEmpty(parentId)
? null
: _libraryManager.GetItemById(parentId);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) BaseItem? item = null;
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length != 1
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) || !(string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
{ {
parentItem = null; item = _libraryManager.GetParentItem(parentId, user?.Id);
} }
var item = string.IsNullOrEmpty(parentId)
? user == null
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder()
: parentItem;
var query = new InternalItemsQuery var query = new InternalItemsQuery
{ {
User = user, User = user,
MediaTypes = (mediaTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries), MediaTypes = mediaTypes,
IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries), IncludeItemTypes = includeItemTypes,
Recursive = true, Recursive = true,
EnableTotalRecordCount = false, EnableTotalRecordCount = false,
DtoOptions = new DtoOptions DtoOptions = new DtoOptions
@ -90,7 +83,12 @@ namespace Jellyfin.Api.Controllers
} }
}; };
var itemList = ((Folder)item!).GetItemList(query); if (item is not Folder folder)
{
return new QueryFiltersLegacy();
}
var itemList = folder.GetItemList(query);
return new QueryFiltersLegacy return new QueryFiltersLegacy
{ {
Years = itemList.Select(i => i.ProductionYear ?? -1) Years = itemList.Select(i => i.ProductionYear ?? -1)
@ -138,8 +136,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryFilters> GetQueryFilters( public ActionResult<QueryFilters> GetQueryFilters(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? parentId, [FromQuery] Guid? parentId,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isAiring, [FromQuery] bool? isAiring,
[FromQuery] bool? isMovie, [FromQuery] bool? isMovie,
[FromQuery] bool? isSports, [FromQuery] bool? isSports,
@ -148,27 +146,28 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isSeries, [FromQuery] bool? isSeries,
[FromQuery] bool? recursive) [FromQuery] bool? recursive)
{ {
var parentItem = string.IsNullOrEmpty(parentId)
? null
: _libraryManager.GetItemById(parentId);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) BaseItem? parentItem = null;
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length == 1
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
{ {
parentItem = null; parentItem = null;
} }
else if (parentId.HasValue)
{
parentItem = _libraryManager.GetItemById(parentId.Value);
}
var filters = new QueryFilters(); var filters = new QueryFilters();
var genreQuery = new InternalItemsQuery(user) var genreQuery = new InternalItemsQuery(user)
{ {
IncludeItemTypes = IncludeItemTypes = includeItemTypes,
(includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
DtoOptions = new DtoOptions DtoOptions = new DtoOptions
{ {
Fields = Array.Empty<ItemFields>(), Fields = Array.Empty<ItemFields>(),
@ -192,10 +191,11 @@ namespace Jellyfin.Api.Controllers
genreQuery.Parent = parentItem; genreQuery.Parent = parentItem;
} }
if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length == 1
|| string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase) && (string.Equals(includeItemTypes[0], nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase) || string.Equals(includeItemTypes[0], nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase)) || string.Equals(includeItemTypes[0], nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes[0], nameof(Audio), StringComparison.OrdinalIgnoreCase)))
{ {
filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
{ {

View File

@ -1,4 +1,4 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
@ -72,10 +72,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -109,15 +109,15 @@ namespace Jellyfin.Api.Controllers
EnableTotalRecordCount = enableTotalRecordCount EnableTotalRecordCount = enableTotalRecordCount
}; };
if (!string.IsNullOrWhiteSpace(parentId)) if (parentId.HasValue)
{ {
if (parentItem is Folder) if (parentItem is Folder)
{ {
query.AncestorIds = new[] { new Guid(parentId) }; query.AncestorIds = new[] { parentId.Value };
} }
else else
{ {
query.ItemIds = new[] { new Guid(parentId) }; query.ItemIds = new[] { parentId.Value };
} }
} }
@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers
result = _libraryManager.GetGenres(query); result = _libraryManager.GetGenres(query);
} }
var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes); var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
@ -112,11 +112,13 @@ namespace Jellyfin.Api.Controllers
/// <param name="segmentId">The segment id.</param> /// <param name="segmentId">The segment id.</param>
/// <param name="segmentContainer">The segment container.</param> /// <param name="segmentContainer">The segment container.</param>
/// <response code="200">Hls video segment returned.</response> /// <response code="200">Hls video segment returned.</response>
/// <response code="404">Hls segment not found.</response>
/// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns> /// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
// [Authenticated] // [Authenticated]
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")] [HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile] [ProducesVideoFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsVideoSegmentLegacy( public ActionResult GetHlsVideoSegmentLegacy(
@ -132,13 +134,25 @@ namespace Jellyfin.Api.Controllers
var normalizedPlaylistId = playlistId; var normalizedPlaylistId = playlistId;
var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath) var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
.FirstOrDefault(i => // Add . to start of segment container for future use.
string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) segmentContainer = segmentContainer.Insert(0, ".");
&& i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) string? playlistPath = null;
?? throw new ResourceNotFoundException($"Provided path ({transcodeFolderPath}) is not valid."); foreach (var path in filePaths)
{
var pathExtension = Path.GetExtension(path);
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
&& path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
{
playlistPath = path;
break;
}
}
return GetFileResult(file, playlistPath); return playlistPath == null
? NotFound("Hls segment not found.")
: GetFileResult(file, playlistPath);
} }
private ActionResult GetFileResult(string path, string playlistPath) private ActionResult GetFileResult(string path, string playlistPath)

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -86,7 +86,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="403">User does not have permission to delete the image.</response> /// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}")] [HttpPost("Users/{userId}/Images/{imageType}")]
[HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
@ -95,7 +94,53 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> PostUserImage( public async Task<ActionResult> PostUserImage(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute] int? index = null) [FromQuery] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
return Forbid("User is not allowed to update the image.");
}
var user = _userManager.GetUserById(userId);
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null)
{
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
}
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Sets the user image.
/// </summary>
/// <param name="userId">User Id.</param>
/// <param name="imageType">(Unused) Image type.</param>
/// <param name="index">(Unused) Image index.</param>
/// <response code="204">Image updated.</response>
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Users/{userId}/Images/{imageType}/{index}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> PostUserImageByIndex(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromRoute] int index)
{ {
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{ {
@ -132,8 +177,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="204">Image deleted.</response> /// <response code="204">Image deleted.</response>
/// <response code="403">User does not have permission to delete the image.</response> /// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Users/{userId}/Images/{itemType}")] [HttpDelete("Users/{userId}/Images/{imageType}")]
[HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@ -142,7 +186,46 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> DeleteUserImage( public async Task<ActionResult> DeleteUserImage(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute] int? index = null) [FromQuery] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
return Forbid("User is not allowed to delete the image.");
}
var user = _userManager.GetUserById(userId);
try
{
System.IO.File.Delete(user.ProfileImage.Path);
}
catch (IOException e)
{
_logger.LogError(e, "Error deleting user profile image:");
}
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Delete the user's image.
/// </summary>
/// <param name="userId">User Id.</param>
/// <param name="imageType">(Unused) Image type.</param>
/// <param name="index">(Unused) Image index.</param>
/// <response code="204">Image deleted.</response>
/// <response code="403">User does not have permission to delete the image.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> DeleteUserImageByIndex(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromRoute] int index)
{ {
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{ {
@ -173,14 +256,13 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns> /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpDelete("Items/{itemId}/Images/{imageType}")] [HttpDelete("Items/{itemId}/Images/{imageType}")]
[HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteItemImage( public async Task<ActionResult> DeleteItemImage(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute] int? imageIndex = null) [FromQuery] int? imageIndex)
{ {
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);
if (item == null) if (item == null)
@ -192,6 +274,65 @@ namespace Jellyfin.Api.Controllers
return NoContent(); return NoContent();
} }
/// <summary>
/// Delete an item's image.
/// </summary>
/// <param name="itemId">Item id.</param>
/// <param name="imageType">Image type.</param>
/// <param name="imageIndex">The image index.</param>
/// <response code="204">Image deleted.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteItemImageByIndex(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType,
[FromRoute] int imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Set item image.
/// </summary>
/// <param name="itemId">Item id.</param>
/// <param name="imageType">Image type.</param>
/// <response code="204">Image saved.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpPost("Items/{itemId}/Images/{imageType}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> SetItemImage(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
}
/// <summary> /// <summary>
/// Set item image. /// Set item image.
/// </summary> /// </summary>
@ -201,16 +342,15 @@ namespace Jellyfin.Api.Controllers
/// <response code="204">Image saved.</response> /// <response code="204">Image saved.</response>
/// <response code="404">Item not found.</response> /// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns> /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
[HttpPost("Items/{itemId}/Images/{imageType}")] [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> SetItemImage( public async Task<ActionResult> SetItemImageByIndex(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute] int? imageIndex = null) [FromRoute] int imageIndex)
{ {
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);
if (item == null) if (item == null)
@ -350,8 +490,6 @@ namespace Jellyfin.Api.Controllers
/// </returns> /// </returns>
[HttpGet("Items/{itemId}/Images/{imageType}")] [HttpGet("Items/{itemId}/Images/{imageType}")]
[HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")] [HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
[HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")]
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile] [ProducesImageFile]
@ -372,7 +510,86 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
[FromRoute] int? imageIndex = null) [FromQuery] int? imageIndex)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)
{
return NotFound();
}
return await GetImageInternal(
itemId,
imageType,
imageIndex,
tag,
format,
maxWidth,
maxHeight,
percentPlayed,
unplayedCount,
width,
height,
quality,
cropWhitespace,
addPlayedIndicator,
blur,
backgroundColor,
foregroundLayer,
item,
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
.ConfigureAwait(false);
}
/// <summary>
/// Gets the item's image.
/// </summary>
/// <param name="itemId">Item id.</param>
/// <param name="imageType">Image type.</param>
/// <param name="imageIndex">Image index.</param>
/// <param name="maxWidth">The maximum image width to return.</param>
/// <param name="maxHeight">The maximum image height to return.</param>
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
/// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
/// <response code="200">Image stream returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
[HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetItemImageByIndex(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] ImageType imageType,
[FromRoute] int imageIndex,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] string? tag,
[FromQuery] bool? cropWhitespace,
[FromQuery] ImageFormat? format,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
{ {
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);
if (item == null) if (item == null)
@ -508,8 +725,8 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success, /// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found. /// or a <see cref="NotFoundResult"/> if item not found.
/// </returns> /// </returns>
[HttpGet("Artists/{name}/Images/{imageType}/{imageIndex?}")] [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
[HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")] [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile] [ProducesImageFile]
@ -587,8 +804,8 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success, /// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found. /// or a <see cref="NotFoundResult"/> if item not found.
/// </returns> /// </returns>
[HttpGet("Genres/{name}/Images/{imageType}/{imageIndex?}")] [HttpGet("Genres/{name}/Images/{imageType}")]
[HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")] [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile] [ProducesImageFile]
@ -609,7 +826,86 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
[FromRoute] int? imageIndex = null) [FromQuery] int? imageIndex)
{
var item = _libraryManager.GetGenre(name);
if (item == null)
{
return NotFound();
}
return await GetImageInternal(
item.Id,
imageType,
imageIndex,
tag,
format,
maxWidth,
maxHeight,
percentPlayed,
unplayedCount,
width,
height,
quality,
cropWhitespace,
addPlayedIndicator,
blur,
backgroundColor,
foregroundLayer,
item,
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
.ConfigureAwait(false);
}
/// <summary>
/// Get genre image by name.
/// </summary>
/// <param name="name">Genre name.</param>
/// <param name="imageType">Image type.</param>
/// <param name="imageIndex">Image index.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
/// <param name="maxWidth">The maximum image width to return.</param>
/// <param name="maxHeight">The maximum image height to return.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
/// <response code="200">Image stream returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
[HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
[HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetGenreImageByIndex(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
{ {
var item = _libraryManager.GetGenre(name); var item = _libraryManager.GetGenre(name);
if (item == null) if (item == null)
@ -666,8 +962,8 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success, /// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found. /// or a <see cref="NotFoundResult"/> if item not found.
/// </returns> /// </returns>
[HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex?}")] [HttpGet("MusicGenres/{name}/Images/{imageType}")]
[HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")] [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile] [ProducesImageFile]
@ -688,7 +984,86 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
[FromRoute] int? imageIndex = null) [FromQuery] int? imageIndex)
{
var item = _libraryManager.GetMusicGenre(name);
if (item == null)
{
return NotFound();
}
return await GetImageInternal(
item.Id,
imageType,
imageIndex,
tag,
format,
maxWidth,
maxHeight,
percentPlayed,
unplayedCount,
width,
height,
quality,
cropWhitespace,
addPlayedIndicator,
blur,
backgroundColor,
foregroundLayer,
item,
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
.ConfigureAwait(false);
}
/// <summary>
/// Get music genre image by name.
/// </summary>
/// <param name="name">Music genre name.</param>
/// <param name="imageType">Image type.</param>
/// <param name="imageIndex">Image index.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
/// <param name="maxWidth">The maximum image width to return.</param>
/// <param name="maxHeight">The maximum image height to return.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
/// <response code="200">Image stream returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
[HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
[HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetMusicGenreImageByIndex(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
{ {
var item = _libraryManager.GetMusicGenre(name); var item = _libraryManager.GetMusicGenre(name);
if (item == null) if (item == null)
@ -745,8 +1120,8 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success, /// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found. /// or a <see cref="NotFoundResult"/> if item not found.
/// </returns> /// </returns>
[HttpGet("Persons/{name}/Images/{imageType}/{imageIndex?}")] [HttpGet("Persons/{name}/Images/{imageType}")]
[HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")] [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile] [ProducesImageFile]
@ -767,7 +1142,86 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
[FromRoute] int? imageIndex = null) [FromQuery] int? imageIndex)
{
var item = _libraryManager.GetPerson(name);
if (item == null)
{
return NotFound();
}
return await GetImageInternal(
item.Id,
imageType,
imageIndex,
tag,
format,
maxWidth,
maxHeight,
percentPlayed,
unplayedCount,
width,
height,
quality,
cropWhitespace,
addPlayedIndicator,
blur,
backgroundColor,
foregroundLayer,
item,
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
.ConfigureAwait(false);
}
/// <summary>
/// Get person image by name.
/// </summary>
/// <param name="name">Person name.</param>
/// <param name="imageType">Image type.</param>
/// <param name="imageIndex">Image index.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
/// <param name="maxWidth">The maximum image width to return.</param>
/// <param name="maxHeight">The maximum image height to return.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
/// <response code="200">Image stream returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
[HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
[HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetPersonImageByIndex(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
{ {
var item = _libraryManager.GetPerson(name); var item = _libraryManager.GetPerson(name);
if (item == null) if (item == null)
@ -824,16 +1278,16 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success, /// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found. /// or a <see cref="NotFoundResult"/> if item not found.
/// </returns> /// </returns>
[HttpGet("Studios/{name}/Images/{imageType}/{imageIndex?}")] [HttpGet("Studios/{name}/Images/{imageType}")]
[HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")] [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile] [ProducesImageFile]
public async Task<ActionResult> GetStudioImage( public async Task<ActionResult> GetStudioImage(
[FromRoute, Required] string name, [FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute, Required] string tag, [FromQuery] string? tag,
[FromRoute, Required] ImageFormat format, [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed, [FromQuery] double? percentPlayed,
@ -846,7 +1300,86 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
[FromRoute] int? imageIndex = null) [FromQuery] int? imageIndex)
{
var item = _libraryManager.GetStudio(name);
if (item == null)
{
return NotFound();
}
return await GetImageInternal(
item.Id,
imageType,
imageIndex,
tag,
format,
maxWidth,
maxHeight,
percentPlayed,
unplayedCount,
width,
height,
quality,
cropWhitespace,
addPlayedIndicator,
blur,
backgroundColor,
foregroundLayer,
item,
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
.ConfigureAwait(false);
}
/// <summary>
/// Get studio image by name.
/// </summary>
/// <param name="name">Studio name.</param>
/// <param name="imageType">Image type.</param>
/// <param name="imageIndex">Image index.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
/// <param name="maxWidth">The maximum image width to return.</param>
/// <param name="maxHeight">The maximum image height to return.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
/// <response code="200">Image stream returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
[HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
[HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetStudioImageByIndex(
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
{ {
var item = _libraryManager.GetStudio(name); var item = _libraryManager.GetStudio(name);
if (item == null) if (item == null)
@ -903,8 +1436,8 @@ namespace Jellyfin.Api.Controllers
/// A <see cref="FileStreamResult"/> containing the file stream on success, /// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found. /// or a <see cref="NotFoundResult"/> if item not found.
/// </returns> /// </returns>
[HttpGet("Users/{userId}/Images/{imageType}/{imageIndex?}")] [HttpGet("Users/{userId}/Images/{imageType}")]
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")] [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile] [ProducesImageFile]
@ -925,7 +1458,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? blur, [FromQuery] int? blur,
[FromQuery] string? backgroundColor, [FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer, [FromQuery] string? foregroundLayer,
[FromRoute] int? imageIndex = null) [FromQuery] int? imageIndex)
{ {
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
if (user == null) if (user == null)
@ -974,6 +1507,103 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false); .ConfigureAwait(false);
} }
/// <summary>
/// Get user profile image.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="imageType">Image type.</param>
/// <param name="imageIndex">Image index.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
/// <param name="maxWidth">The maximum image width to return.</param>
/// <param name="maxHeight">The maximum image height to return.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
/// <param name="width">The fixed image width to return.</param>
/// <param name="height">The fixed image height to return.</param>
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="blur">Optional. Blur image.</param>
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
/// <response code="200">Image stream returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>
/// A <see cref="FileStreamResult"/> containing the file stream on success,
/// or a <see cref="NotFoundResult"/> if item not found.
/// </returns>
[HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public async Task<ActionResult> GetUserImageByIndex(
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] int imageIndex,
[FromQuery] string? tag,
[FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
[FromQuery] int? width,
[FromQuery] int? height,
[FromQuery] int? quality,
[FromQuery] bool? cropWhitespace,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] int? blur,
[FromQuery] string? backgroundColor,
[FromQuery] string? foregroundLayer)
{
var user = _userManager.GetUserById(userId);
if (user?.ProfileImage == null)
{
return NotFound();
}
var info = new ItemImageInfo
{
Path = user.ProfileImage.Path,
Type = ImageType.Profile,
DateModified = user.ProfileImage.LastModified
};
if (width.HasValue)
{
info.Width = width.Value;
}
if (height.HasValue)
{
info.Height = height.Value;
}
return await GetImageInternal(
user.Id,
imageType,
imageIndex,
tag,
format,
maxWidth,
maxHeight,
percentPlayed,
unplayedCount,
width,
height,
quality,
cropWhitespace,
addPlayedIndicator,
blur,
backgroundColor,
foregroundLayer,
null,
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase),
info)
.ConfigureAwait(false);
}
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream) private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
{ {
using var reader = new StreamReader(inputStream); using var reader = new StreamReader(inputStream);

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response> /// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("Artists/InstantMix")] [HttpGet("Artists/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists( public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
[FromRoute, Required] Guid id, [FromRoute, Required] Guid id,
@ -242,7 +242,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response> /// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("MusicGenres/InstantMix")] [HttpGet("MusicGenres/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres( public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
[FromRoute, Required] Guid id, [FromRoute, Required] Guid id,

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.IO; using System.IO;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;

View File

@ -60,7 +60,6 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Gets items based on a query. /// Gets items based on a query.
/// </summary> /// </summary>
/// <param name="uId">The user id supplied in the /Users/{uid}/Items.</param>
/// <param name="userId">The user id supplied as query parameter.</param> /// <param name="userId">The user id supplied as query parameter.</param>
/// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
/// <param name="hasThemeSong">Optional filter by items with theme songs.</param> /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
@ -143,10 +142,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
[HttpGet("Items")] [HttpGet("Items")]
[HttpGet("Users/{uId}/Items", Name = "GetItems_2")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetItems( public ActionResult<QueryResult<BaseItemDto>> GetItems(
[FromRoute] Guid? uId,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? maxOfficialRating, [FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong, [FromQuery] bool? hasThemeSong,
@ -159,7 +156,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasParentalRating, [FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd, [FromQuery] bool? isHd,
[FromQuery] bool? is4K, [FromQuery] bool? is4K,
[FromQuery] string? locationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing, [FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired, [FromQuery] bool? isUnaired,
@ -173,42 +170,42 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId, [FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId, [FromQuery] bool? hasTvdbId,
[FromQuery] string? excludeItemIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool? recursive, [FromQuery] bool? recursive,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery] string? tags, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery] string? years, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? studios, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery] string? artists, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
[FromQuery] string? excludeArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery] string? artistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
[FromQuery] string? albumArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
[FromQuery] string? contributingArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
[FromQuery] string? albums, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
[FromQuery] string? albumIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
[FromQuery] string? ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] string? videoTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating, [FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked, [FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder, [FromQuery] bool? isPlaceHolder,
@ -219,18 +216,15 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
[FromQuery] bool? is3D, [FromQuery] bool? is3D,
[FromQuery] string? seriesStatus, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan, [FromQuery] string? nameLessThan,
[FromQuery] string? studioIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
{ {
// use user id route parameter over query parameter
userId = uId ?? userId;
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
@ -238,20 +232,15 @@ namespace Jellyfin.Api.Controllers
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length == 1
|| string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase)) && (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase)
|| includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase)))
{ {
parentId = null; parentId = null;
} }
BaseItem? item = null; var item = _libraryManager.GetParentItem(parentId, userId);
QueryResult<BaseItem> result; QueryResult<BaseItem> result;
if (!string.IsNullOrEmpty(parentId))
{
item = _libraryManager.GetItemById(parentId);
}
item ??= _libraryManager.GetUserRootFolder();
if (!(item is Folder folder)) if (!(item is Folder folder))
{ {
@ -262,7 +251,7 @@ namespace Jellyfin.Api.Controllers
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{ {
recursive = true; recursive = true;
includeItemTypes = "Playlist"; includeItemTypes = new[] { "Playlist" };
} }
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
@ -291,14 +280,14 @@ namespace Jellyfin.Api.Controllers
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
} }
if ((recursive.HasValue && recursive.Value) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder)) if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder))
{ {
var query = new InternalItemsQuery(user!) var query = new InternalItemsQuery(user!)
{ {
IsPlayed = isPlayed, IsPlayed = isPlayed,
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), MediaTypes = mediaTypes,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
Recursive = recursive ?? false, Recursive = recursive ?? false,
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -330,28 +319,28 @@ namespace Jellyfin.Api.Controllers
HasTrailer = hasTrailer, HasTrailer = hasTrailer,
IsHD = isHd, IsHD = isHd,
Is4K = is4K, Is4K = is4K,
Tags = RequestHelpers.Split(tags, '|', true), Tags = tags,
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), OfficialRatings = officialRatings,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
ArtistIds = RequestHelpers.GetGuids(artistIds), ArtistIds = artistIds,
AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds), AlbumArtistIds = albumArtistIds,
ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds), ContributingArtistIds = contributingArtistIds,
GenreIds = RequestHelpers.GetGuids(genreIds), GenreIds = genreIds,
StudioIds = RequestHelpers.GetGuids(studioIds), StudioIds = studioIds,
Person = person, Person = person,
PersonIds = RequestHelpers.GetGuids(personIds), PersonIds = personIds,
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), Years = years,
ImageTypes = imageTypes, ImageTypes = imageTypes,
VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(), VideoTypes = videoTypes,
AdjacentTo = adjacentTo, AdjacentTo = adjacentTo,
ItemIds = RequestHelpers.GetGuids(ids), ItemIds = ids,
MinCommunityRating = minCommunityRating, MinCommunityRating = minCommunityRating,
MinCriticRating = minCriticRating, MinCriticRating = minCriticRating,
ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId), ParentId = parentId ?? Guid.Empty,
ParentIndexNumber = parentIndexNumber, ParentIndexNumber = parentIndexNumber,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds), ExcludeItemIds = excludeItemIds,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
MinDateLastSaved = minDateLastSaved?.ToUniversalTime(), MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
@ -360,7 +349,7 @@ namespace Jellyfin.Api.Controllers
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(), MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
}; };
if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm)) if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
{ {
query.CollapseBoxSetItems = false; query.CollapseBoxSetItems = false;
} }
@ -400,9 +389,9 @@ namespace Jellyfin.Api.Controllers
} }
// Filter by Series Status // Filter by Series Status
if (!string.IsNullOrEmpty(seriesStatus)) if (seriesStatus.Length != 0)
{ {
query.SeriesStatuses = seriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray(); query.SeriesStatuses = seriesStatus;
} }
// ExcludeLocationTypes // ExcludeLocationTypes
@ -411,13 +400,9 @@ namespace Jellyfin.Api.Controllers
query.IsVirtualItem = false; query.IsVirtualItem = false;
} }
if (!string.IsNullOrEmpty(locationTypes)) if (locationTypes.Length > 0 && locationTypes.Length < 4)
{ {
var requestedLocationTypes = locationTypes.Split(','); query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
{
query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
}
} }
// Min official rating // Min official rating
@ -433,9 +418,9 @@ namespace Jellyfin.Api.Controllers
} }
// Artists // Artists
if (!string.IsNullOrEmpty(artists)) if (artists.Length != 0)
{ {
query.ArtistIds = artists.Split('|').Select(i => query.ArtistIds = artists.Select(i =>
{ {
try try
{ {
@ -449,29 +434,29 @@ namespace Jellyfin.Api.Controllers
} }
// ExcludeArtistIds // ExcludeArtistIds
if (!string.IsNullOrWhiteSpace(excludeArtistIds)) if (excludeArtistIds.Length != 0)
{ {
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds); query.ExcludeArtistIds = excludeArtistIds;
} }
if (!string.IsNullOrWhiteSpace(albumIds)) if (albumIds.Length != 0)
{ {
query.AlbumIds = RequestHelpers.GetGuids(albumIds); query.AlbumIds = albumIds;
} }
// Albums // Albums
if (!string.IsNullOrEmpty(albums)) if (albums.Length != 0)
{ {
query.AlbumIds = albums.Split('|').SelectMany(i => query.AlbumIds = albums.SelectMany(i =>
{ {
return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 }); return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
}).ToArray(); }).ToArray();
} }
// Studios // Studios
if (!string.IsNullOrEmpty(studios)) if (studios.Length != 0)
{ {
query.StudioIds = studios.Split('|').Select(i => query.StudioIds = studios.Select(i =>
{ {
try try
{ {
@ -505,6 +490,257 @@ namespace Jellyfin.Api.Controllers
return new QueryResult<BaseItemDto> { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) }; return new QueryResult<BaseItemDto> { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) };
} }
/// <summary>
/// Gets items based on a query.
/// </summary>
/// <param name="userId">The user id supplied as query parameter.</param>
/// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
/// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
/// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
/// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
/// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
/// <param name="hasTrailer">Optional filter by items with trailers.</param>
/// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
/// <param name="parentIndexNumber">Optional filter by parent index number.</param>
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
/// <param name="isHd">Optional filter by items that are HD or not.</param>
/// <param name="is4K">Optional filter by items that are 4K or not.</param>
/// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
/// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
/// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
/// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
/// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
/// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="isPlayed">Optional filter by items that are played, or not.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</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>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param>
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
/// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
/// <param name="isLocked">Optional filter by items that are locked.</param>
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
/// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
/// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
/// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
/// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
/// <param name="is3D">Optional filter by items that are 3D, or not.</param>
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
[HttpGet("Users/{userId}/Items")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId(
[FromRoute] Guid userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
[FromQuery] bool? hasThemeVideo,
[FromQuery] bool? hasSubtitles,
[FromQuery] bool? hasSpecialFeature,
[FromQuery] bool? hasTrailer,
[FromQuery] string? adjacentTo,
[FromQuery] int? parentIndexNumber,
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
[FromQuery] double? minCriticRating,
[FromQuery] DateTime? minPremiereDate,
[FromQuery] DateTime? minDateLastSaved,
[FromQuery] DateTime? minDateLastSavedForUser,
[FromQuery] DateTime? maxPremiereDate,
[FromQuery] bool? hasOverview,
[FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] bool? recursive,
[FromQuery] string? searchTerm,
[FromQuery] string? sortOrder,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder,
[FromQuery] bool? hasOfficialRating,
[FromQuery] bool? collapseBoxSetItems,
[FromQuery] int? minWidth,
[FromQuery] int? minHeight,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] bool? is3D,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
return GetItems(
userId,
maxOfficialRating,
hasThemeSong,
hasThemeVideo,
hasSubtitles,
hasSpecialFeature,
hasTrailer,
adjacentTo,
parentIndexNumber,
hasParentalRating,
isHd,
is4K,
locationTypes,
excludeLocationTypes,
isMissing,
isUnaired,
minCommunityRating,
minCriticRating,
minPremiereDate,
minDateLastSaved,
minDateLastSavedForUser,
maxPremiereDate,
hasOverview,
hasImdbId,
hasTmdbId,
hasTvdbId,
excludeItemIds,
startIndex,
limit,
recursive,
searchTerm,
sortOrder,
parentId,
fields,
excludeItemTypes,
includeItemTypes,
filters,
isFavorite,
mediaTypes,
imageTypes,
sortBy,
isPlayed,
genres,
officialRatings,
tags,
years,
enableUserData,
imageTypeLimit,
enableImageTypes,
person,
personIds,
personTypes,
studios,
artists,
excludeArtistIds,
artistIds,
albumArtistIds,
contributingArtistIds,
albums,
albumIds,
ids,
videoTypes,
minOfficialRating,
isLocked,
isPlaceHolder,
hasOfficialRating,
collapseBoxSetItems,
minWidth,
minHeight,
maxWidth,
maxHeight,
is3D,
seriesStatus,
nameStartsWithOrGreater,
nameStartsWith,
nameLessThan,
studioIds,
genreIds,
enableTotalRecordCount,
enableImages);
}
/// <summary> /// <summary>
/// Gets items based on a query. /// Gets items based on a query.
/// </summary> /// </summary>
@ -531,19 +767,19 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
{ {
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId); var parentIdGuid = parentId ?? Guid.Empty;
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -569,13 +805,13 @@ namespace Jellyfin.Api.Controllers
ParentId = parentIdGuid, ParentId = parentIdGuid,
Recursive = true, Recursive = true,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), MediaTypes = mediaTypes,
IsVirtualItem = false, IsVirtualItem = false,
CollapseBoxSetItems = false, CollapseBoxSetItems = false,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
AncestorIds = ancestorIds, AncestorIds = ancestorIds,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
SearchTerm = searchTerm SearchTerm = searchTerm
}); });

View File

@ -362,15 +362,14 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public ActionResult DeleteItems([FromQuery] string? ids) public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{ {
if (string.IsNullOrEmpty(ids)) if (ids.Length == 0)
{ {
return NoContent(); return NoContent();
} }
var itemIds = RequestHelpers.Split(ids, ',', true); foreach (var i in ids)
foreach (var i in itemIds)
{ {
var item = _libraryManager.GetItemById(i); var item = _libraryManager.GetItemById(i);
var auth = _authContext.GetAuthorizationInfo(Request); var auth = _authContext.GetAuthorizationInfo(Request);
@ -691,7 +690,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems( public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery] string? excludeArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
@ -753,9 +752,9 @@ namespace Jellyfin.Api.Controllers
}; };
// ExcludeArtistIds // ExcludeArtistIds
if (!string.IsNullOrEmpty(excludeArtistIds)) if (excludeArtistIds.Length != 0)
{ {
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds); query.ExcludeArtistIds = excludeArtistIds;
} }
List<BaseItem> itemsResult = _libraryManager.GetItemList(query); List<BaseItem> itemsResult = _libraryManager.GetItemList(query);

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -17,7 +17,6 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
@ -150,7 +149,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] string? sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] SortOrder? sortOrder, [FromQuery] SortOrder? sortOrder,
[FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true) [FromQuery] bool addCurrentProgram = true)
@ -175,7 +174,7 @@ namespace Jellyfin.Api.Controllers
IsNews = isNews, IsNews = isNews,
IsKids = isKids, IsKids = isKids,
IsSports = isSports, IsSports = isSports,
SortBy = RequestHelpers.Split(sortBy, ',', true), SortBy = sortBy,
SortOrder = sortOrder ?? SortOrder.Ascending, SortOrder = sortOrder ?? SortOrder.Ascending,
AddCurrentProgram = addCurrentProgram AddCurrentProgram = addCurrentProgram
}, },
@ -539,7 +538,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms( public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
[FromQuery] string? channelIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] DateTime? minStartDate, [FromQuery] DateTime? minStartDate,
[FromQuery] bool? hasAired, [FromQuery] bool? hasAired,
@ -556,8 +555,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@ -573,8 +572,7 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ChannelIds = RequestHelpers.Split(channelIds, ',', true) ChannelIds = channelIds,
.Select(i => new Guid(i)).ToArray(),
HasAired = hasAired, HasAired = hasAired,
IsAiring = isAiring, IsAiring = isAiring,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
@ -591,8 +589,8 @@ namespace Jellyfin.Api.Controllers
IsKids = isKids, IsKids = isKids,
IsSports = isSports, IsSports = isSports,
SeriesTimerId = seriesTimerId, SeriesTimerId = seriesTimerId,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
GenreIds = RequestHelpers.GetGuids(genreIds) GenreIds = genreIds
}; };
if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty)) if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
@ -628,8 +626,7 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true) ChannelIds = body.ChannelIds,
.Select(i => new Guid(i)).ToArray(),
HasAired = body.HasAired, HasAired = body.HasAired,
IsAiring = body.IsAiring, IsAiring = body.IsAiring,
EnableTotalRecordCount = body.EnableTotalRecordCount, EnableTotalRecordCount = body.EnableTotalRecordCount,
@ -646,8 +643,8 @@ namespace Jellyfin.Api.Controllers
IsKids = body.IsKids, IsKids = body.IsKids,
IsSports = body.IsSports, IsSports = body.IsSports,
SeriesTimerId = body.SeriesTimerId, SeriesTimerId = body.SeriesTimerId,
Genres = RequestHelpers.Split(body.Genres, '|', true), Genres = body.Genres,
GenreIds = RequestHelpers.GetGuids(body.GenreIds) GenreIds = body.GenreIds
}; };
if (!body.LibrarySeriesId.Equals(Guid.Empty)) if (!body.LibrarySeriesId.Equals(Guid.Empty))
@ -703,7 +700,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
@ -723,7 +720,7 @@ namespace Jellyfin.Api.Controllers
IsNews = isNews, IsNews = isNews,
IsSports = isSports, IsSports = isSports,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
GenreIds = RequestHelpers.GetGuids(genreIds) GenreIds = genreIds
}; };
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
@ -1017,7 +1014,9 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(pw)) if (!string.IsNullOrEmpty(pw))
{ {
using var sha = SHA1.Create(); using var sha = SHA1.Create();
listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw))); // TODO: remove ToLower when Convert.ToHexString supports lowercase
// Schedules Direct requires the hex to be lowercase
listingsProviderInfo.Password = Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
} }
return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false); return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
@ -8,7 +8,6 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.MediaInfoDtos; using Jellyfin.Api.Models.MediaInfoDtos;
using Jellyfin.Api.Models.VideoDtos;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -81,6 +80,9 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Gets live playback media info for an item. /// Gets live playback media info for an item.
/// </summary> /// </summary>
/// <remarks>
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
/// </remarks>
/// <param name="itemId">The item id.</param> /// <param name="itemId">The item id.</param>
/// <param name="userId">The user id.</param> /// <param name="userId">The user id.</param>
/// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param> /// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
@ -90,13 +92,13 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxAudioChannels">The maximum number of audio channels.</param> /// <param name="maxAudioChannels">The maximum number of audio channels.</param>
/// <param name="mediaSourceId">The media source id.</param> /// <param name="mediaSourceId">The media source id.</param>
/// <param name="liveStreamId">The livestream id.</param> /// <param name="liveStreamId">The livestream id.</param>
/// <param name="deviceProfile">The device profile.</param>
/// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param> /// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param>
/// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param> /// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
/// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param> /// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
/// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param> /// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param>
/// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param> /// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param>
/// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param> /// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
/// <param name="playbackInfoDto">The playback info.</param>
/// <response code="200">Playback info returned.</response> /// <response code="200">Playback info returned.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns> /// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
[HttpPost("Items/{itemId}/PlaybackInfo")] [HttpPost("Items/{itemId}/PlaybackInfo")]
@ -111,18 +113,17 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxAudioChannels, [FromQuery] int? maxAudioChannels,
[FromQuery] string? mediaSourceId, [FromQuery] string? mediaSourceId,
[FromQuery] string? liveStreamId, [FromQuery] string? liveStreamId,
[FromBody] DeviceProfileDto? deviceProfile, [FromQuery] bool? autoOpenLiveStream,
[FromQuery] bool autoOpenLiveStream = false, [FromQuery] bool? enableDirectPlay,
[FromQuery] bool enableDirectPlay = true, [FromQuery] bool? enableDirectStream,
[FromQuery] bool enableDirectStream = true, [FromQuery] bool? enableTranscoding,
[FromQuery] bool enableTranscoding = true, [FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool allowVideoStreamCopy = true, [FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool allowAudioStreamCopy = true) [FromBody] PlaybackInfoDto? playbackInfoDto)
{ {
var authInfo = _authContext.GetAuthorizationInfo(Request); var authInfo = _authContext.GetAuthorizationInfo(Request);
var profile = deviceProfile?.DeviceProfile; var profile = playbackInfoDto?.DeviceProfile;
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile); _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
if (profile == null) if (profile == null)
@ -134,6 +135,23 @@ namespace Jellyfin.Api.Controllers
} }
} }
// Copy params from posted body
// TODO clean up when breaking API compatibility.
userId ??= playbackInfoDto?.UserId;
maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate;
startTimeTicks ??= playbackInfoDto?.StartTimeTicks;
audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex;
subtitleStreamIndex ??= playbackInfoDto?.SubtitleStreamIndex;
maxAudioChannels ??= playbackInfoDto?.MaxAudioChannels;
mediaSourceId ??= playbackInfoDto?.MediaSourceId;
liveStreamId ??= playbackInfoDto?.LiveStreamId;
autoOpenLiveStream ??= playbackInfoDto?.AutoOpenLiveStream ?? false;
enableDirectPlay ??= playbackInfoDto?.EnableDirectPlay ?? true;
enableDirectStream ??= playbackInfoDto?.EnableDirectStream ?? true;
enableTranscoding ??= playbackInfoDto?.EnableTranscoding ?? true;
allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true;
allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? true;
var info = await _mediaInfoHelper.GetPlaybackInfo( var info = await _mediaInfoHelper.GetPlaybackInfo(
itemId, itemId,
userId, userId,
@ -161,18 +179,18 @@ namespace Jellyfin.Api.Controllers
maxAudioChannels, maxAudioChannels,
info!.PlaySessionId!, info!.PlaySessionId!,
userId ?? Guid.Empty, userId ?? Guid.Empty,
enableDirectPlay, enableDirectPlay.Value,
enableDirectStream, enableDirectStream.Value,
enableTranscoding, enableTranscoding.Value,
allowVideoStreamCopy, allowVideoStreamCopy.Value,
allowAudioStreamCopy, allowAudioStreamCopy.Value,
Request.HttpContext.GetNormalizedRemoteIp()); Request.HttpContext.GetNormalizedRemoteIp());
} }
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
} }
if (autoOpenLiveStream) if (autoOpenLiveStream.Value)
{ {
var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal)); var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal));
@ -183,9 +201,9 @@ namespace Jellyfin.Api.Controllers
new LiveStreamRequest new LiveStreamRequest
{ {
AudioStreamIndex = audioStreamIndex, AudioStreamIndex = audioStreamIndex,
DeviceProfile = deviceProfile?.DeviceProfile, DeviceProfile = playbackInfoDto?.DeviceProfile,
EnableDirectPlay = enableDirectPlay, EnableDirectPlay = enableDirectPlay.Value,
EnableDirectStream = enableDirectStream, EnableDirectStream = enableDirectStream.Value,
ItemId = itemId, ItemId = itemId,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
MaxStreamingBitrate = maxStreamingBitrate, MaxStreamingBitrate = maxStreamingBitrate,

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Recommendations")] [HttpGet("Recommendations")]
public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations( public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] int categoryLimit = 5, [FromQuery] int categoryLimit = 5,
[FromQuery] int itemLimit = 8) [FromQuery] int itemLimit = 8)
@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers
var categories = new List<RecommendationDto>(); var categories = new List<RecommendationDto>();
var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId); var parentIdGuid = parentId ?? Guid.Empty;
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {

View File

@ -1,4 +1,4 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
@ -72,10 +72,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -109,21 +109,21 @@ namespace Jellyfin.Api.Controllers
EnableTotalRecordCount = enableTotalRecordCount EnableTotalRecordCount = enableTotalRecordCount
}; };
if (!string.IsNullOrWhiteSpace(parentId)) if (parentId.HasValue)
{ {
if (parentItem is Folder) if (parentItem is Folder)
{ {
query.AncestorIds = new[] { new Guid(parentId) }; query.AncestorIds = new[] { parentId.Value };
} }
else else
{ {
query.ItemIds = new[] { new Guid(parentId) }; query.ItemIds = new[] { parentId.Value };
} }
} }
var result = _libraryManager.GetMusicGenres(query); var result = _libraryManager.GetMusicGenres(query);
var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes); var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
} }

View File

@ -45,13 +45,13 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PackageInfo>> GetPackageInfo( public async Task<ActionResult<PackageInfo>> GetPackageInfo(
[FromRoute, Required] string name, [FromRoute, Required] string name,
[FromQuery] string? assemblyGuid) [FromQuery] Guid? assemblyGuid)
{ {
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
var result = _installationManager.FilterPackages( var result = _installationManager.FilterPackages(
packages, packages,
name, name,
string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)) assemblyGuid ?? default)
.FirstOrDefault(); .FirstOrDefault();
if (result == null) if (result == null)
@ -92,21 +92,21 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
public async Task<ActionResult> InstallPackage( public async Task<ActionResult> InstallPackage(
[FromRoute, Required] string name, [FromRoute, Required] string name,
[FromQuery] string? assemblyGuid, [FromQuery] Guid? assemblyGuid,
[FromQuery] string? version, [FromQuery] string? version,
[FromQuery] string? repositoryUrl) [FromQuery] string? repositoryUrl)
{ {
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
if (!string.IsNullOrEmpty(repositoryUrl)) if (!string.IsNullOrEmpty(repositoryUrl))
{ {
packages = packages.Where(p => p.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)) packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any())
.ToList(); .ToList();
} }
var package = _installationManager.GetCompatibleVersions( var package = _installationManager.GetCompatibleVersions(
packages, packages,
name, name,
string.IsNullOrEmpty(assemblyGuid) ? Guid.Empty : Guid.Parse(assemblyGuid), assemblyGuid ?? Guid.Empty,
specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version)) specificVersion: string.IsNullOrEmpty(version) ? null : Version.Parse(version))
.FirstOrDefault(); .FirstOrDefault();

View File

@ -1,4 +1,4 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
@ -77,9 +77,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? excludePersonTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? appearsInItemId, [FromQuery] Guid? appearsInItemId,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
{ {
@ -97,12 +97,12 @@ namespace Jellyfin.Api.Controllers
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
{ {
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true), ExcludePersonTypes = excludePersonTypes,
NameContains = searchTerm, NameContains = searchTerm,
User = user, User = user,
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
AppearsInItemId = string.IsNullOrEmpty(appearsInItemId) ? Guid.Empty : Guid.Parse(appearsInItemId), AppearsInItemId = appearsInItemId ?? Guid.Empty,
Limit = limit ?? 0 Limit = limit ?? 0
}); });

View File

@ -1,4 +1,4 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -63,11 +63,10 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
[FromBody, Required] CreatePlaylistDto createPlaylistRequest) [FromBody, Required] CreatePlaylistDto createPlaylistRequest)
{ {
Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{ {
Name = createPlaylistRequest.Name, Name = createPlaylistRequest.Name,
ItemIdList = idGuidArray, ItemIdList = createPlaylistRequest.Ids,
UserId = createPlaylistRequest.UserId, UserId = createPlaylistRequest.UserId,
MediaType = createPlaylistRequest.MediaType MediaType = createPlaylistRequest.MediaType
}).ConfigureAwait(false); }).ConfigureAwait(false);
@ -87,10 +86,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddToPlaylist( public async Task<ActionResult> AddToPlaylist(
[FromRoute, Required] Guid playlistId, [FromRoute, Required] Guid playlistId,
[FromQuery] string? ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] Guid? userId) [FromQuery] Guid? userId)
{ {
await _playlistManager.AddToPlaylistAsync(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty).ConfigureAwait(false); await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -122,9 +121,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns> /// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")] [HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveFromPlaylist([FromRoute, Required] string playlistId, [FromQuery] string? entryIds) public async Task<ActionResult> RemoveFromPlaylist(
[FromRoute, Required] string playlistId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
{ {
await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false); await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
return NoContent(); return NoContent();
} }

View File

@ -1,9 +1,10 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
@ -74,7 +75,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<UserItemDataDto> MarkPlayedItem( public ActionResult<UserItemDataDto> MarkPlayedItem(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery] DateTime? datePlayed) [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
{ {
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request); var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;

View File

@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -82,10 +83,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery, Required] string searchTerm, [FromQuery, Required] string searchTerm,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? parentId, [FromQuery] Guid? parentId,
[FromQuery] bool? isMovie, [FromQuery] bool? isMovie,
[FromQuery] bool? isSeries, [FromQuery] bool? isSeries,
[FromQuery] bool? isNews, [FromQuery] bool? isNews,
@ -108,9 +109,9 @@ namespace Jellyfin.Api.Controllers
IncludeStudios = includeStudios, IncludeStudios = includeStudios,
StartIndex = startIndex, StartIndex = startIndex,
UserId = userId ?? Guid.Empty, UserId = userId ?? Guid.Empty,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), MediaTypes = mediaTypes,
ParentId = parentId, ParentId = parentId,
IsKids = isKids, IsKids = isKids,

View File

@ -6,6 +6,7 @@ using System.Threading;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.SessionDtos;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -160,12 +161,12 @@ namespace Jellyfin.Api.Controllers
public ActionResult Play( public ActionResult Play(
[FromRoute, Required] string sessionId, [FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand, [FromQuery, Required] PlayCommand playCommand,
[FromQuery, Required] string itemIds, [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
[FromQuery] long? startPositionTicks) [FromQuery] long? startPositionTicks)
{ {
var playRequest = new PlayRequest var playRequest = new PlayRequest
{ {
ItemIds = RequestHelpers.GetGuids(itemIds), ItemIds = itemIds,
StartPositionTicks = startPositionTicks, StartPositionTicks = startPositionTicks,
PlayCommand = playCommand PlayCommand = playCommand
}; };
@ -378,7 +379,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostCapabilities( public ActionResult PostCapabilities(
[FromQuery] string? id, [FromQuery] string? id,
[FromQuery] string? playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false, [FromQuery] bool supportsSync = false,
@ -391,7 +392,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.ReportCapabilities(id, new ClientCapabilities _sessionManager.ReportCapabilities(id, new ClientCapabilities
{ {
PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true), PlayableMediaTypes = playableMediaTypes,
SupportedCommands = supportedCommands, SupportedCommands = supportedCommands,
SupportsMediaControl = supportsMediaControl, SupportsMediaControl = supportsMediaControl,
SupportsSync = supportsSync, SupportsSync = supportsSync,
@ -412,14 +413,14 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostFullCapabilities( public ActionResult PostFullCapabilities(
[FromQuery] string? id, [FromQuery] string? id,
[FromBody, Required] ClientCapabilities capabilities) [FromBody, Required] ClientCapabilitiesDto capabilities)
{ {
if (string.IsNullOrWhiteSpace(id)) if (string.IsNullOrWhiteSpace(id))
{ {
id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
} }
_sessionManager.ReportCapabilities(id, capabilities); _sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
return NoContent(); return NoContent();
} }

View File

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.StartupDtos; using Jellyfin.Api.Models.StartupDtos;
using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -72,9 +73,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration) public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration)
{ {
_config.Configuration.UICulture = startupConfiguration.UICulture; _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty;
_config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode; _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty;
_config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage; _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty;
_config.SaveConfiguration(); _config.SaveConfiguration();
return NoContent(); return NoContent();
} }
@ -89,9 +90,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto) public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto)
{ {
_config.Configuration.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess; NetworkConfiguration settings = _config.GetNetworkConfiguration();
_config.Configuration.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping; settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
_config.SaveConfiguration(); settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
_config.SaveConfiguration("network", settings);
return NoContent(); return NoContent();
} }

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