Merge remote-tracking branch 'upstream/master' into NetworkPR2

pull/4125/head
Greenback 4 years ago
commit ebe650afa9

@ -0,0 +1,62 @@
parameters:
- name: LinuxImage
type: string
default: "ubuntu-latest"
- name: GeneratorVersion
type: string
default: "5.0.0-beta2"
jobs:
- job: GenerateApiClients
displayName: 'Generate Api Clients'
dependsOn: Test
pool:
vmImage: "${{ parameters.LinuxImage }}"
steps:
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec Artifact'
inputs:
source: 'current'
artifact: "OpenAPI Spec"
path: "$(System.ArtifactsDirectory)/openapispec"
runVersion: "latest"
- task: CmdLine@2
displayName: 'Download OpenApi Generator'
inputs:
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
# Generate npm api client
# Unstable
- task: CmdLine@2
displayName: 'Build unstable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)"
- task: Npm@1
displayName: 'Publish unstable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
command: publish
publishRegistry: useFeed
publishFeed: 'unstable@Local'
workingDir: ./apiclient/generated/typescript/axios
# Stable
- task: CmdLine@2
displayName: 'Build stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
- task: Npm@1
displayName: 'Publish stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
command: publish
publishRegistry: useExternalRegistry
publishEndpoint: 'jellyfin-bot for NPM'
workingDir: ./apiclient/generated/typescript/axios

@ -63,7 +63,38 @@ jobs:
sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: OpenAPISpec
dependsOn: Test
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
displayName: 'Push OpenAPI Spec to repository'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DownloadPipelineArtifact@2
displayName: 'Download OpenAPI Spec'
inputs:
source: 'current'
artifact: "OpenAPI Spec"
path: "$(System.ArtifactsDirectory)/openapispec"
runVersion: "latest"
- task: SSH@0
displayName: 'Create target directory on repository server'
inputs:
sshEndpoint: repository
runOptions: 'inline'
inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
- task: CopyFilesOverSSH@0
displayName: 'Upload artifacts to repository server'
inputs:
sshEndpoint: repository
sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
contents: 'openapi.json'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
- job: BuildDocker
displayName: 'Build Docker'

@ -56,7 +56,7 @@ jobs:
inputs:
command: "test"
projects: ${{ parameters.TestProjects }}
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
publishTestResults: true
testRunTitle: $(Agent.JobName)
workingDirectory: "$(Build.SourcesDirectory)"

@ -34,6 +34,12 @@ jobs:
Linux: 'ubuntu-latest'
Windows: 'windows-latest'
macOS: 'macos-latest'
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-test.yml
parameters:
ImageNames:
Linux: 'ubuntu-latest'
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-abi.yml
@ -55,3 +61,6 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-api-client.yml

1
.gitignore vendored

@ -276,3 +276,4 @@ BenchmarkDotNet.Artifacts
web/
web-src.*
MediaBrowser.WebDashboard/jellyfin-web
apiclient/generated

@ -137,6 +137,7 @@
- [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
- [skyfrk](https://github.com/skyfrk)
# Emby Contributors

@ -811,7 +811,7 @@ namespace Emby.Dlna.PlayTo
}
/// <inheritdoc />
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
{
if (_disposed)
{
@ -823,17 +823,17 @@ namespace Emby.Dlna.PlayTo
return Task.CompletedTask;
}
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
if (name == SessionMessageType.Play)
{
return SendPlayCommand(data as PlayRequest, cancellationToken);
}
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
if (name == SessionMessageType.PlayState)
{
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
}
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
if (name == SessionMessageType.GeneralCommand)
{
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
}

@ -209,15 +209,15 @@ namespace Emby.Dlna.PlayTo
SupportedCommands = new[]
{
GeneralCommandType.VolumeDown.ToString(),
GeneralCommandType.VolumeUp.ToString(),
GeneralCommandType.Mute.ToString(),
GeneralCommandType.Unmute.ToString(),
GeneralCommandType.ToggleMute.ToString(),
GeneralCommandType.SetVolume.ToString(),
GeneralCommandType.SetAudioStreamIndex.ToString(),
GeneralCommandType.SetSubtitleStreamIndex.ToString(),
GeneralCommandType.PlayMediaSource.ToString()
GeneralCommandType.VolumeDown,
GeneralCommandType.VolumeUp,
GeneralCommandType.Mute,
GeneralCommandType.Unmute,
GeneralCommandType.ToggleMute,
GeneralCommandType.SetVolume,
GeneralCommandType.SetAudioStreamIndex,
GeneralCommandType.SetSubtitleStreamIndex,
GeneralCommandType.PlayMediaSource
},
SupportsMediaControl = true

@ -101,6 +101,7 @@ using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.DataProtection.Repositories;
@ -549,6 +550,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TvdbClientManager>();
ServiceCollection.AddSingleton<TmdbClientManager>();
ServiceCollection.AddSingleton(NetManager);

@ -16,6 +16,7 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
@ -105,7 +106,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
_sessionManager.SendMessageToAdminSessions("RefreshProgress", dict, CancellationToken.None);
_sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
}
catch
{
@ -123,7 +124,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
_sessionManager.SendMessageToAdminSessions("RefreshProgress", collectionFolderDict, CancellationToken.None);
_sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
}
catch
{
@ -345,7 +346,7 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "LibraryChanged", info, cancellationToken).ConfigureAwait(false);
await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{

@ -10,6 +10,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
@ -46,25 +47,25 @@ namespace Emby.Server.Implementations.EntryPoints
private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
{
await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false);
}
private async Task SendMessage(string name, TimerEventInfo info)
private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
{
var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();

@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints
private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken)
{
return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "UserDataChanged", () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
}
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.HttpServer
Connection = this
};
if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
if (info.MessageType == SessionMessageType.KeepAlive)
{
await SendKeepAliveResponse().ConfigureAwait(false);
}
@ -244,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer
new WebSocketMessage<string>
{
MessageId = Guid.NewGuid(),
MessageType = "KeepAlive"
MessageType = SessionMessageType.KeepAlive
}, CancellationToken.None);
}

@ -148,7 +148,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
public bool IsHidden => false;
/// <inheritdoc />
public bool IsEnabled => false;
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;

@ -1064,10 +1064,10 @@ namespace Emby.Server.Implementations.Session
AssertCanControl(session, controllingSession);
}
return SendMessageToSession(session, "GeneralCommand", command, cancellationToken);
return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
}
private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, CancellationToken cancellationToken)
{
var controllers = session.SessionControllers;
var messageId = Guid.NewGuid();
@ -1078,7 +1078,7 @@ namespace Emby.Server.Implementations.Session
}
}
private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, string name, T data, CancellationToken cancellationToken)
private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data, CancellationToken cancellationToken)
{
IEnumerable<Task> GetTasks()
{
@ -1178,7 +1178,7 @@ namespace Emby.Server.Implementations.Session
}
}
await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@ -1186,7 +1186,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
var session = GetSessionToRemoteControl(sessionId);
await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false);
await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@ -1194,7 +1194,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
var session = GetSessionToRemoteControl(sessionId);
await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false);
await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false);
}
private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
}
}
return SendMessageToSession(session, "Playstate", command, cancellationToken);
return SendMessageToSession(session, SessionMessageType.PlayState, command, cancellationToken);
}
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
@ -1322,7 +1322,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
return SendMessageToSessions(Sessions, "RestartRequired", string.Empty, cancellationToken);
return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
}
/// <summary>
@ -1334,7 +1334,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
return SendMessageToSessions(Sessions, "ServerShuttingDown", string.Empty, cancellationToken);
return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
}
/// <summary>
@ -1348,7 +1348,7 @@ namespace Emby.Server.Implementations.Session
_logger.LogDebug("Beginning SendServerRestartNotification");
return SendMessageToSessions(Sessions, "ServerRestarting", string.Empty, cancellationToken);
return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
}
/// <summary>
@ -1484,6 +1484,14 @@ namespace Emby.Server.Implementations.Session
throw new SecurityException("User is not allowed access from this device.");
}
int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
int maxActiveSessions = user.MaxActiveSessions;
_logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions);
if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
{
throw new SecurityException("User is at their maximum number of sessions.");
}
var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName);
var session = LogSessionActivity(
@ -1866,7 +1874,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken)
public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1879,7 +1887,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken)
public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1894,7 +1902,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken)
public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1903,7 +1911,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();

@ -8,6 +8,7 @@ using Jellyfin.Data.Events;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@ -316,7 +317,7 @@ namespace Emby.Server.Implementations.Session
return webSocket.SendAsync(
new WebSocketMessage<int>
{
MessageType = "ForceKeepAlive",
MessageType = SessionMessageType.ForceKeepAlive,
Data = WebSocketLostTimeout
},
CancellationToken.None);

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session
@ -65,7 +66,7 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public Task SendMessage<T>(
string name,
SessionMessageType name,
Guid messageId,
T data,
CancellationToken cancellationToken)

@ -301,8 +301,7 @@ namespace Emby.Server.Implementations.SyncPlay
if (_group.IsPaused)
{
// Pick a suitable time that accounts for latency
var delay = _group.GetHighestPing() * 2;
delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
var delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing);
// Unpause group and set starting point in future
// Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
@ -452,8 +451,7 @@ namespace Emby.Server.Implementations.SyncPlay
else
{
// Client, that was buffering, resumed playback but did not update others in time
delay = _group.GetHighestPing() * 2;
delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing);
_group.LastActivity = currentTime.AddMilliseconds(
delay);
@ -497,7 +495,7 @@ namespace Emby.Server.Implementations.SyncPlay
private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
{
// Collected pings are used to account for network latency when unpausing playback
_group.UpdatePing(session, request.Ping ?? _group.DefaultPing);
_group.UpdatePing(session, request.Ping ?? GroupInfo.DefaultPing);
}
/// <inheritdoc />

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
@ -378,7 +379,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult PostCapabilities(
[FromQuery] string? id,
[FromQuery] string? playableMediaTypes,
[FromQuery] string? supportedCommands,
[FromQuery] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false,
[FromQuery] bool supportsPersistentIdentifier = true)
@ -391,7 +392,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.ReportCapabilities(id, new ClientCapabilities
{
PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true),
SupportedCommands = RequestHelpers.Split(supportedCommands, ',', true),
SupportedCommands = supportedCommands,
SupportsMediaControl = supportsMediaControl,
SupportsSync = supportsSync,
SupportsPersistentIdentifier = supportsPersistentIdentifier

@ -554,7 +554,7 @@ namespace Jellyfin.Api.Helpers
private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress)
{
var maxBitrate = clientMaxBitrate;
var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
if (remoteClientMaxBitrate <= 0)
{

@ -740,10 +740,7 @@ namespace Jellyfin.Api.Helpers
/// <param name="state">The state.</param>
private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
{
if (job != null)
{
job.HasExited = true;
}
job.HasExited = true;
_logger.LogDebug("Disposing stream resources");
state.Dispose();

@ -0,0 +1,64 @@
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Jellyfin.Api.ModelBinders
{
/// <summary>
/// Comma delimited array model binder.
/// Returns an empty array of specified type if there is no query parameter.
/// </summary>
public class CommaDelimitedArrayModelBinder : IModelBinder
{
/// <inheritdoc/>
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var elementType = bindingContext.ModelType.GetElementType();
var converter = TypeDescriptor.GetConverter(elementType);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
if (valueProviderResult.Length > 1)
{
var result = Array.CreateInstance(elementType, valueProviderResult.Length);
for (int i = 0; i < valueProviderResult.Length; i++)
{
var value = converter.ConvertFromString(valueProviderResult.Values[i].Trim());
result.SetValue(value, i);
}
bindingContext.Result = ModelBindingResult.Success(result);
}
else
{
var value = valueProviderResult.FirstValue;
if (value != null)
{
var values = Array.ConvertAll(
value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries),
x => converter.ConvertFromString(x?.Trim()));
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Result = ModelBindingResult.Success(typedValues);
}
else
{
var emptyResult = Array.CreateInstance(elementType, 0);
bindingContext.Result = ModelBindingResult.Success(emptyResult);
}
}
return Task.CompletedTask;
}
}
}

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Jellyfin.Api.ModelBinders
{
/// <summary>
/// Comma delimited array model binder provider.
/// </summary>
public class CommaDelimitedArrayModelBinderProvider : IModelBinderProvider
{
private readonly IModelBinder _binder;
/// <summary>
/// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinderProvider"/> class.
/// </summary>
public CommaDelimitedArrayModelBinderProvider()
{
_binder = new CommaDelimitedArrayModelBinder();
}
/// <inheritdoc />
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
return context.Metadata.ModelType.IsArray ? _binder : null;
}
}
}

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.WebSocketListeners
@ -29,11 +30,14 @@ namespace Jellyfin.Api.WebSocketListeners
_activityManager.EntryCreated += OnEntryCreated;
}
/// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
protected override string Name => "ActivityLogEntry";
/// <inheritdoc />
protected override SessionMessageType Type => SessionMessageType.ActivityLogEntry;
/// <inheritdoc />
protected override SessionMessageType StartType => SessionMessageType.ActivityLogEntryStart;
/// <inheritdoc />
protected override SessionMessageType StopType => SessionMessageType.ActivityLogEntryStop;
/// <summary>
/// Gets the data to send.

@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@ -33,11 +34,14 @@ namespace Jellyfin.Api.WebSocketListeners
_taskManager.TaskCompleted += OnTaskCompleted;
}
/// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
protected override string Name => "ScheduledTasksInfo";
/// <inheritdoc />
protected override SessionMessageType Type => SessionMessageType.ScheduledTasksInfo;
/// <inheritdoc />
protected override SessionMessageType StartType => SessionMessageType.ScheduledTasksInfoStart;
/// <inheritdoc />
protected override SessionMessageType StopType => SessionMessageType.ScheduledTasksInfoStop;
/// <summary>
/// Gets the data to send.

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.WebSocketListeners
@ -34,7 +35,13 @@ namespace Jellyfin.Api.WebSocketListeners
}
/// <inheritdoc />
protected override string Name => "Sessions";
protected override SessionMessageType Type => SessionMessageType.Sessions;
/// <inheritdoc />
protected override SessionMessageType StartType => SessionMessageType.SessionsStart;
/// <inheritdoc />
protected override SessionMessageType StopType => SessionMessageType.SessionsStop;
/// <summary>
/// Gets the data to send.

@ -188,6 +188,11 @@ namespace Jellyfin.Data.Entities
/// </summary>
public int? LoginAttemptsBeforeLockout { get; set; }
/// <summary>
/// Gets or sets the maximum number of active sessions the user can have at once.
/// </summary>
public int MaxActiveSessions { get; set; }
/// <summary>
/// Gets or sets the subtitle mode.
/// </summary>

@ -2,6 +2,7 @@
using System.Threading.Tasks;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Tasks;
namespace Jellyfin.Server.Implementations.Events.Consumers.System
@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System
/// <inheritdoc />
public async Task OnEvent(TaskCompletionEventArgs eventArgs)
{
await _sessionManager.SendMessageToAdminSessions("ScheduledTaskEnded", eventArgs.Result, CancellationToken.None).ConfigureAwait(false);
await _sessionManager.SendMessageToAdminSessions(SessionMessageType.ScheduledTaskEnded, eventArgs.Result, CancellationToken.None).ConfigureAwait(false);
}
}
}

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
{
@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
/// <inheritdoc />
public async Task OnEvent(PluginInstallationCancelledEventArgs eventArgs)
{
await _sessionManager.SendMessageToAdminSessions("PackageInstallationCancelled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationCancelled, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
}
}
}

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
{
@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
/// <inheritdoc />
public async Task OnEvent(InstallationFailedEventArgs eventArgs)
{
await _sessionManager.SendMessageToAdminSessions("PackageInstallationFailed", eventArgs.InstallationInfo, CancellationToken.None).ConfigureAwait(false);
await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationFailed, eventArgs.InstallationInfo, CancellationToken.None).ConfigureAwait(false);
}
}
}

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
{
@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
/// <inheritdoc />
public async Task OnEvent(PluginInstalledEventArgs eventArgs)
{
await _sessionManager.SendMessageToAdminSessions("PackageInstallationCompleted", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationCompleted, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
}
}
}

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
{
@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
/// <inheritdoc />
public async Task OnEvent(PluginInstallingEventArgs eventArgs)
{
await _sessionManager.SendMessageToAdminSessions("PackageInstalling", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstalling, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
}
}
}

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
{
@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
/// <inheritdoc />
public async Task OnEvent(PluginUninstalledEventArgs eventArgs)
{
await _sessionManager.SendMessageToAdminSessions("PluginUninstalled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageUninstalled, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
}
}
}

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Events.Users;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Users
{
@ -30,7 +31,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
{
await _sessionManager.SendMessageToUserSessions(
new List<Guid> { eventArgs.Argument.Id },
"UserDeleted",
SessionMessageType.UserDeleted,
eventArgs.Argument.Id.ToString("N", CultureInfo.InvariantCulture),
CancellationToken.None).ConfigureAwait(false);
}

@ -6,6 +6,7 @@ using Jellyfin.Data.Events.Users;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
namespace Jellyfin.Server.Implementations.Events.Consumers.Users
{
@ -33,7 +34,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
{
await _sessionManager.SendMessageToUserSessions(
new List<Guid> { e.Argument.Id },
"UserUpdated",
SessionMessageType.UserUpdated,
_userManager.GetUserDto(e.Argument),
CancellationToken.None).ConfigureAwait(false);
}

@ -0,0 +1,464 @@
#pragma warning disable CS1591
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDb))]
[Migration("20201004171403_AddMaxActiveSessions")]
partial class AddMaxActiveSessions
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "3.1.8");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DayOfWeek")
.HasColumnType("INTEGER");
b.Property<double>("EndHour")
.HasColumnType("REAL");
b.Property<double>("StartHour")
.HasColumnType("REAL");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AccessSchedules");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<string>("Overview")
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ActivityLogs");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChromecastVersion")
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<string>("DashboardTheme")
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<bool>("EnableNextVideoInfoOverlay")
.HasColumnType("INTEGER");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<int>("ScrollDirection")
.HasColumnType("INTEGER");
b.Property<bool>("ShowBackdrop")
.HasColumnType("INTEGER");
b.Property<bool>("ShowSidebar")
.HasColumnType("INTEGER");
b.Property<int>("SkipBackwardLength")
.HasColumnType("INTEGER");
b.Property<int>("SkipForwardLength")
.HasColumnType("INTEGER");
b.Property<string>("TvHome")
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("UserId", "Client")
.IsUnique();
b.ToTable("DisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DisplayPreferencesId")
.HasColumnType("INTEGER");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("DisplayPreferencesId");
b.ToTable("HomeSection");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("ImageInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<bool>("RememberIndexing")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSorting")
.HasColumnType("INTEGER");
b.Property<string>("SortBy")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(64);
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<int>("ViewType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_Permissions_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<bool>("Value")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Permission_Permissions_Guid");
b.ToTable("Permissions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Preference_Preferences_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.HasKey("Id");
b.HasIndex("Preference_Preferences_Guid");
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AudioLanguagePreference")
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<string>("AuthenticationProviderId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<string>("EasyPassword")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
b.Property<bool>("EnableLocalPassword")
.HasColumnType("INTEGER");
b.Property<bool>("EnableNextEpisodeAutoPlay")
.HasColumnType("INTEGER");
b.Property<bool>("EnableUserPreferenceAccess")
.HasColumnType("INTEGER");
b.Property<bool>("HidePlayedInLatest")
.HasColumnType("INTEGER");
b.Property<long>("InternalId")
.HasColumnType("INTEGER");
b.Property<int>("InvalidLoginAttemptCount")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastActivityDate")
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLoginDate")
.HasColumnType("TEXT");
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
b.Property<int?>("MaxActiveSessions")
.HasColumnType("INTEGER");
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<bool>("PlayDefaultAudioTrack")
.HasColumnType("INTEGER");
b.Property<bool>("RememberAudioSelections")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSubtitleSelections")
.HasColumnType("INTEGER");
b.Property<int?>("RemoteClientBitrateLimit")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SubtitleLanguagePreference")
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int>("SyncPlayAccess")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("AccessSchedules")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("DisplayPreferences")
.HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
.WithMany("HomeSections")
.HasForeignKey("DisplayPreferencesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("ItemDisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("Permission_Permissions_Guid");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("Preference_Preferences_Guid");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,28 @@
#pragma warning disable CS1591
#pragma warning disable SA1601
using Microsoft.EntityFrameworkCore.Migrations;
namespace Jellyfin.Server.Implementations.Migrations
{
public partial class AddMaxActiveSessions : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "MaxActiveSessions",
schema: "jellyfin",
table: "Users",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MaxActiveSessions",
schema: "jellyfin",
table: "Users");
}
}
}

@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "3.1.7");
.HasAnnotation("ProductVersion", "3.1.8");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@ -344,6 +344,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
b.Property<int>("MaxActiveSessions")
.HasColumnType("INTEGER");
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");

@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Users
public string Name => "InvalidOrMissingAuthenticationProvider";
/// <inheritdoc />
public bool IsEnabled => true;
public bool IsEnabled => false;
/// <inheritdoc />
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)

@ -379,6 +379,7 @@ namespace Jellyfin.Server.Implementations.Users
PasswordResetProviderId = user.PasswordResetProviderId,
InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
MaxActiveSessions = user.MaxActiveSessions,
IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
IsHidden = user.HasPermission(PermissionKind.IsHidden),
IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
@ -701,6 +702,7 @@ namespace Jellyfin.Server.Implementations.Users
user.PasswordResetProviderId = policy.PasswordResetProviderId;
user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
user.LoginAttemptsBeforeLockout = maxLoginAttempts;
user.MaxActiveSessions = policy.MaxActiveSessions;
user.SyncPlayAccess = policy.SyncPlayAccess;
user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
@ -799,7 +801,7 @@ namespace Jellyfin.Server.Implementations.Users
private IList<IPasswordResetProvider> GetPasswordResetProviders(User user)
{
var passwordResetProviderId = user?.PasswordResetProviderId;
var passwordResetProviderId = user.PasswordResetProviderId;
var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
if (!string.IsNullOrEmpty(passwordResetProviderId))

@ -16,6 +16,7 @@ using Jellyfin.Api.Auth.LocalAccessPolicy;
using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Server.Configuration;
using Jellyfin.Server.Filters;
using Jellyfin.Server.Formatters;
@ -166,6 +167,8 @@ namespace Jellyfin.Server.Extensions
opts.OutputFormatters.Add(new CssOutputFormatter());
opts.OutputFormatters.Add(new XmlOutputFormatter());
opts.ModelBinderProviders.Insert(0, new CommaDelimitedArrayModelBinderProvider());
})
// Clear app parts to avoid other assemblies being picked up

@ -125,8 +125,8 @@ namespace Jellyfin.Server.Middleware
switch (ex)
{
case ArgumentException _: return StatusCodes.Status400BadRequest;
case AuthenticationException _:
case SecurityException _: return StatusCodes.Status401Unauthorized;
case AuthenticationException _: return StatusCodes.Status401Unauthorized;
case SecurityException _: return StatusCodes.Status403Forbidden;
case DirectoryNotFoundException _:
case FileNotFoundException _:
case ResourceNotFoundException _: return StatusCodes.Status404NotFound;

@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using System.Net.Http.Headers;
using System.Net.Mime;
using Jellyfin.Api.TypeConverters;
using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Extensions;
@ -12,6 +13,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
@ -124,10 +126,15 @@ namespace Jellyfin.Server
mainApp.UseStaticFiles();
if (appConfig.HostWebClient())
{
var extensionProvider = new FileExtensionContentTypeProvider();
// subtitles octopus requires .data files.
extensionProvider.Mappings.Add(".data", MediaTypeNames.Application.Octet);
mainApp.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(_serverConfigurationManager.ApplicationPaths.WebPath),
RequestPath = "/web"
RequestPath = "/web",
ContentTypeProvider = extensionProvider
});
}

@ -8,37 +8,38 @@ namespace MediaBrowser.Common.Json.Converters
/// Converts a nullable struct or value to/from JSON.
/// Required - some clients send an empty string.
/// </summary>
/// <typeparam name="T">The struct type.</typeparam>
public class JsonNullableStructConverter<T> : JsonConverter<T?>
where T : struct
/// <typeparam name="TStruct">The struct type.</typeparam>
public class JsonNullableStructConverter<TStruct> : JsonConverter<TStruct?>
where TStruct : struct
{
private readonly JsonConverter<T?> _baseJsonConverter;
/// <summary>
/// Initializes a new instance of the <see cref="JsonNullableStructConverter{T}"/> class.
/// </summary>
/// <param name="baseJsonConverter">The base json converter.</param>
public JsonNullableStructConverter(JsonConverter<T?> baseJsonConverter)
{
_baseJsonConverter = baseJsonConverter;
}
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Handle empty string.
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
// Token is empty string.
if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty))
{
return null;
}
return _baseJsonConverter.Read(ref reader, typeToConvert, options);
return JsonSerializer.Deserialize<TStruct>(ref reader, options);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options)
{
_baseJsonConverter.Write(writer, value, options);
if (value.HasValue)
{
JsonSerializer.Serialize(writer, value.Value, options);
}
else
{
writer.WriteNullValue();
}
}
}
}

@ -0,0 +1,27 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Json nullable struct converter factory.
/// </summary>
public class JsonNullableStructConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsGenericType
&& typeToConvert.GetGenericTypeDefinition() == typeof(Nullable<>)
&& typeToConvert.GenericTypeArguments[0].IsValueType;
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GenericTypeArguments[0];
return (JsonConverter)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType));
}
}
}

@ -39,14 +39,9 @@ namespace MediaBrowser.Common.Json
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
// Get built-in converters for fallback converting.
var baseNullableInt32Converter = (JsonConverter<int?>)options.GetConverter(typeof(int?));
var baseNullableInt64Converter = (JsonConverter<long?>)options.GetConverter(typeof(long?));
options.Converters.Add(new JsonGuidConverter());
options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new JsonNullableStructConverter<int>(baseNullableInt32Converter));
options.Converters.Add(new JsonNullableStructConverter<long>(baseNullableInt64Converter));
options.Converters.Add(new JsonNullableStructConverterFactory());
return options;
}

@ -8,6 +8,7 @@ using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Net
@ -28,10 +29,22 @@ namespace MediaBrowser.Controller.Net
new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>();
/// <summary>
/// Gets the name.
/// Gets the type used for the messages sent to the client.
/// </summary>
/// <value>The name.</value>
protected abstract string Name { get; }
/// <value>The type.</value>
protected abstract SessionMessageType Type { get; }
/// <summary>
/// Gets the message type received from the client to start sending messages.
/// </summary>
/// <value>The type.</value>
protected abstract SessionMessageType StartType { get; }
/// <summary>
/// Gets the message type received from the client to stop sending messages.
/// </summary>
/// <value>The type.</value>
protected abstract SessionMessageType StopType { get; }
/// <summary>
/// Gets the data to send.
@ -66,12 +79,12 @@ namespace MediaBrowser.Controller.Net
throw new ArgumentNullException(nameof(message));
}
if (string.Equals(message.MessageType, Name + "Start", StringComparison.OrdinalIgnoreCase))
if (message.MessageType == StartType)
{
Start(message);
}
if (string.Equals(message.MessageType, Name + "Stop", StringComparison.OrdinalIgnoreCase))
if (message.MessageType == StopType)
{
Stop(message);
}
@ -159,7 +172,7 @@ namespace MediaBrowser.Controller.Net
new WebSocketMessage<TReturnDataType>
{
MessageId = Guid.NewGuid(),
MessageType = Name,
MessageType = Type,
Data = data
},
cancellationToken).ConfigureAwait(false);
@ -176,7 +189,7 @@ namespace MediaBrowser.Controller.Net
}
catch (Exception ex)
{
Logger.LogError(ex, "Error sending web socket message {Name}", Name);
Logger.LogError(ex, "Error sending web socket message {Name}", Type);
DisposeConnection(tuple);
}
}

@ -3,6 +3,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Controller.Session
{
@ -23,6 +24,6 @@ namespace MediaBrowser.Controller.Session
/// <summary>
/// Sends the message.
/// </summary>
Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken);
Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken);
}
}

@ -188,16 +188,16 @@ namespace MediaBrowser.Controller.Session
/// <param name="data">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken);
Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken);
/// <summary>
/// Sends the message to user sessions.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>Task.</returns>
Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken);
Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken);
Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken);
Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken);
/// <summary>
/// Sends the message to user device sessions.
@ -208,7 +208,7 @@ namespace MediaBrowser.Controller.Session
/// <param name="data">The data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken);
Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken);
/// <summary>
/// Sends the restart required message.

@ -230,8 +230,8 @@ namespace MediaBrowser.Controller.Session
/// Gets or sets the supported commands.
/// </summary>
/// <value>The supported commands.</value>
public string[] SupportedCommands
=> Capabilities == null ? Array.Empty<string>() : Capabilities.SupportedCommands;
public GeneralCommandType[] SupportedCommands
=> Capabilities == null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands;
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
{

@ -14,12 +14,12 @@ namespace MediaBrowser.Controller.SyncPlay
public class GroupInfo
{
/// <summary>
/// Gets the default ping value used for sessions.
/// The default ping value used for sessions.
/// </summary>
public long DefaultPing { get; } = 500;
public const long DefaultPing = 500;
/// <summary>
/// Gets or sets the group identifier.
/// Gets the group identifier.
/// </summary>
/// <value>The group identifier.</value>
public Guid GroupId { get; } = Guid.NewGuid();
@ -58,7 +58,8 @@ namespace MediaBrowser.Controller.SyncPlay
/// <summary>
/// Checks if a session is in this group.
/// </summary>
/// <value><c>true</c> if the session is in this group; <c>false</c> otherwise.</value>
/// <param name="sessionId">The session id to check.</param>
/// <returns><c>true</c> if the session is in this group; <c>false</c> otherwise.</returns>
public bool ContainsSession(string sessionId)
{
return Participants.ContainsKey(sessionId);
@ -70,16 +71,14 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param>
public void AddSession(SessionInfo session)
{
if (ContainsSession(session.Id))
{
return;
}
var member = new GroupMember();
member.Session = session;
member.Ping = DefaultPing;
member.IsBuffering = false;
Participants[session.Id] = member;
Participants.TryAdd(
session.Id,
new GroupMember
{
Session = session,
Ping = DefaultPing,
IsBuffering = false
});
}
/// <summary>
@ -88,12 +87,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param>
public void RemoveSession(SessionInfo session)
{
if (!ContainsSession(session.Id))
{
return;
}
Participants.Remove(session.Id, out _);
Participants.Remove(session.Id);
}
/// <summary>
@ -103,18 +97,16 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="ping">The ping.</param>
public void UpdatePing(SessionInfo session, long ping)
{
if (!ContainsSession(session.Id))
if (Participants.TryGetValue(session.Id, out GroupMember value))
{
return;
value.Ping = ping;
}
Participants[session.Id].Ping = ping;
}
/// <summary>
/// Gets the highest ping in the group.
/// </summary>
/// <value name="session">The highest ping in the group.</value>
/// <returns>The highest ping in the group.</returns>
public long GetHighestPing()
{
long max = long.MinValue;
@ -133,18 +125,16 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="isBuffering">The state.</param>
public void SetBuffering(SessionInfo session, bool isBuffering)
{
if (!ContainsSession(session.Id))
if (Participants.TryGetValue(session.Id, out GroupMember value))
{
return;
value.IsBuffering = isBuffering;
}
Participants[session.Id].IsBuffering = isBuffering;
}
/// <summary>
/// Gets the group buffering state.
/// </summary>
/// <value><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</value>
/// <returns><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</returns>
public bool IsBuffering()
{
foreach (var session in Participants.Values)
@ -161,7 +151,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// <summary>
/// Checks if the group is empty.
/// </summary>
/// <value><c>true</c> if the group is empty; <c>false</c> otherwise.</value>
/// <returns><c>true</c> if the group is empty; <c>false</c> otherwise.</returns>
public bool IsEmpty()
{
return Participants.Count == 0;

@ -25,8 +25,6 @@ namespace MediaBrowser.Model.Configuration
public bool EnableInternetProviders { get; set; }
public bool ImportMissingEpisodes { get; set; }
public bool EnableAutomaticSeriesGrouping { get; set; }
public bool EnableEmbeddedTitles { get; set; }

@ -455,9 +455,10 @@ namespace MediaBrowser.Model.Dlna
if (directPlayProfile == null)
{
_logger.LogInformation("Profile: {0}, No direct play profiles found for Path: {1}",
_logger.LogInformation("Profile: {0}, No audio direct play profiles found for {1} with codec {2}",
options.Profile.Name ?? "Unknown Profile",
item.Path ?? "Unknown path");
item.Path ?? "Unknown path",
audioStream.Codec ?? "Unknown codec");
return (Enumerable.Empty<PlayMethod>(), GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
}
@ -972,9 +973,10 @@ namespace MediaBrowser.Model.Dlna
if (directPlay == null)
{
_logger.LogInformation("Profile: {0}, No direct play profiles found for Path: {1}",
_logger.LogInformation("Profile: {0}, No video direct play profiles found for {1} with codec {2}",
profile.Name ?? "Unknown Profile",
mediaSource.Path ?? "Unknown path");
mediaSource.Path ?? "Unknown path",
videoStream.Codec ?? "Unknown codec");
return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles));
}

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Model.Extensions
{
/// <summary>
/// Extension methods for <see cref="IEnumerable{T}"/>.
/// </summary>
public static class EnumerableExtensions
{
/// <summary>
/// Orders <see cref="RemoteImageInfo"/> by requested language in descending order, prioritizing "en" over other non-matches.
/// </summary>
/// <param name="remoteImageInfos">The remote image infos.</param>
/// <param name="requestedLanguage">The requested language for the images.</param>
/// <returns>The ordered remote image infos.</returns>
public static IEnumerable<RemoteImageInfo> OrderByLanguageDescending(this IEnumerable<RemoteImageInfo> remoteImageInfos, string requestedLanguage)
{
var isRequestedLanguageEn = string.Equals(requestedLanguage, "en", StringComparison.OrdinalIgnoreCase);
return remoteImageInfos.OrderByDescending(i =>
{
if (string.Equals(requestedLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (!isRequestedLanguageEn && string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
{
return 2;
}
if (string.IsNullOrEmpty(i.Language))
{
return isRequestedLanguageEn ? 3 : 2;
}
return 0;
})
.ThenByDescending(i => i.CommunityRating ?? 0)
.ThenByDescending(i => i.VoteCount ?? 0);
}
}
}

@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
using MediaBrowser.Model.Session;
namespace MediaBrowser.Model.Net
{
@ -15,7 +16,7 @@ namespace MediaBrowser.Model.Net
/// Gets or sets the type of the message.
/// </summary>
/// <value>The type of the message.</value>
public string MessageType { get; set; }
public SessionMessageType MessageType { get; set; }
public Guid MessageId { get; set; }

@ -10,7 +10,7 @@ namespace MediaBrowser.Model.Session
{
public string[] PlayableMediaTypes { get; set; }
public string[] SupportedCommands { get; set; }
public GeneralCommandType[] SupportedCommands { get; set; }
public bool SupportsMediaControl { get; set; }
@ -31,7 +31,7 @@ namespace MediaBrowser.Model.Session
public ClientCapabilities()
{
PlayableMediaTypes = Array.Empty<string>();
SupportedCommands = Array.Empty<string>();
SupportedCommands = Array.Empty<GeneralCommandType>();
SupportsPersistentIdentifier = true;
}
}

@ -0,0 +1,50 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Session
{
/// <summary>
/// The different kinds of messages that are used in the WebSocket api.
/// </summary>
public enum SessionMessageType
{
// Server -> Client
ForceKeepAlive,
GeneralCommand,
UserDataChanged,
Sessions,
Play,
SyncPlayCommand,
SyncPlayGroupUpdate,
PlayState,
RestartRequired,
ServerShuttingDown,
ServerRestarting,
LibraryChanged,
UserDeleted,
UserUpdated,
SeriesTimerCreated,
TimerCreated,
SeriesTimerCancelled,
TimerCancelled,
RefreshProgress,
ScheduledTaskEnded,
PackageInstallationCancelled,
PackageInstallationFailed,
PackageInstallationCompleted,
PackageInstalling,
PackageUninstalled,
ActivityLogEntry,
ScheduledTasksInfo,
// Client -> Server
ActivityLogEntryStart,
ActivityLogEntryStop,
SessionsStart,
SessionsStop,
ScheduledTasksInfoStart,
ScheduledTasksInfoStop,
// Shared
KeepAlive,
}
}

@ -92,6 +92,8 @@ namespace MediaBrowser.Model.Users
public int LoginAttemptsBeforeLockout { get; set; }
public int MaxActiveSessions { get; set; }
public bool EnablePublicSharing { get; set; }
public Guid[] BlockedMediaFolders { get; set; }
@ -144,6 +146,8 @@ namespace MediaBrowser.Model.Users
LoginAttemptsBeforeLockout = -1;
MaxActiveSessions = 0;
EnableAllChannels = true;
EnabledChannels = Array.Empty<Guid>();

@ -158,6 +158,14 @@ namespace MediaBrowser.Providers.Manager
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (response.StatusCode != HttpStatusCode.OK)
{
throw new HttpException("Invalid image received.")
{
StatusCode = response.StatusCode
};
}
var contentType = response.Content.Headers.ContentType.MediaType;
// Workaround for tvheadend channel icons

@ -21,6 +21,7 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
<PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.2" />
<PackageReference Include="TMDbLib" Version="1.7.3-alpha" />
<PackageReference Include="TvDbSharper" Version="3.2.2" />
</ItemGroup>

@ -80,32 +80,6 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
return TryGetValue(cacheKey, language, () => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken));
}
public async Task<List<EpisodeRecord>> GetAllEpisodesAsync(int tvdbId, string language,
CancellationToken cancellationToken)
{
// Traverse all episode pages and join them together
var episodes = new List<EpisodeRecord>();
var episodePage = await GetEpisodesPageAsync(tvdbId, new EpisodeQuery(), language, cancellationToken)
.ConfigureAwait(false);
episodes.AddRange(episodePage.Data);
if (!episodePage.Links.Next.HasValue || !episodePage.Links.Last.HasValue)
{
return episodes;
}
int next = episodePage.Links.Next.Value;
int last = episodePage.Links.Last.Value;
for (var page = next; page <= last; ++page)
{
episodePage = await GetEpisodesPageAsync(tvdbId, page, new EpisodeQuery(), language, cancellationToken)
.ConfigureAwait(false);
episodes.AddRange(episodePage.Data);
}
return episodes;
}
public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByImdbIdAsync(
string imdbId,
string language,

@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
@ -12,25 +13,25 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections;
using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
using MediaBrowser.Providers.Plugins.Tmdb.Movies;
namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
{
public class TmdbBoxSetImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly TmdbClientManager _tmdbClientManager;
public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory)
public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
_httpClientFactory = httpClientFactory;
_tmdbClientManager = tmdbClientManager;
}
public string Name => ProviderName;
public string Name => TmdbUtils.ProviderName;
public static string ProviderName => TmdbUtils.ProviderName;
public int Order => 0;
public bool Supports(BaseItem item)
{
@ -48,112 +49,60 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
var tmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
if (!string.IsNullOrEmpty(tmdbId))
if (tmdbId <= 0)
{
var language = item.GetPreferredMetadataLanguage();
var mainResult = await TmdbBoxSetProvider.Current.GetMovieDbResult(tmdbId, null, cancellationToken).ConfigureAwait(false);
return Enumerable.Empty<RemoteImageInfo>();
}
if (mainResult != null)
{
var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
var language = item.GetPreferredMetadataLanguage();
var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
return GetImages(mainResult, language, tmdbImageUrl);
}
if (collection?.Images == null)
{
return Enumerable.Empty<RemoteImageInfo>();
}
return new List<RemoteImageInfo>();
}
var remoteImages = new List<RemoteImageInfo>();
private IEnumerable<RemoteImageInfo> GetImages(CollectionResult obj, string language, string baseUrl)
{
var list = new List<RemoteImageInfo>();
var images = obj.Images ?? new CollectionImages();
list.AddRange(GetPosters(images).Select(i => new RemoteImageInfo
{
Url = baseUrl + i.File_Path,
CommunityRating = i.Vote_Average,
VoteCount = i.Vote_Count,
Width = i.Width,
Height = i.Height,
Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
ProviderName = Name,
Type = ImageType.Primary,
RatingType = RatingType.Score
}));
list.AddRange(GetBackdrops(images).Select(i => new RemoteImageInfo
for (var i = 0; i < collection.Images.Posters.Count; i++)
{
Url = baseUrl + i.File_Path,
CommunityRating = i.Vote_Average,
VoteCount = i.Vote_Count,
Width = i.Width,
Height = i.Height,
ProviderName = Name,
Type = ImageType.Backdrop,
RatingType = RatingType.Score
}));
var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
return list.OrderByDescending(i =>
{
if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (!isLanguageEn)
var poster = collection.Images.Posters[i];
remoteImages.Add(new RemoteImageInfo
{
if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
{
return 2;
}
}
Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
CommunityRating = poster.VoteAverage,
VoteCount = poster.VoteCount,
Width = poster.Width,
Height = poster.Height,
Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
ProviderName = Name,
Type = ImageType.Primary,
RatingType = RatingType.Score
});
}
if (string.IsNullOrEmpty(i.Language))
for (var i = 0; i < collection.Images.Backdrops.Count; i++)
{
var backdrop = collection.Images.Backdrops[i];
remoteImages.Add(new RemoteImageInfo
{
return isLanguageEn ? 3 : 2;
}
return 0;
})
.ThenByDescending(i => i.CommunityRating ?? 0)
.ThenByDescending(i => i.VoteCount ?? 0);
}
/// <summary>
/// Gets the posters.
/// </summary>
/// <param name="images">The images.</param>
/// <returns>IEnumerable{MovieDbProvider.Poster}.</returns>
private IEnumerable<Poster> GetPosters(CollectionImages images)
{
return images.Posters ?? new List<Poster>();
}
/// <summary>
/// Gets the backdrops.
/// </summary>
/// <param name="images">The images.</param>
/// <returns>IEnumerable{MovieDbProvider.Backdrop}.</returns>
private IEnumerable<Backdrop> GetBackdrops(CollectionImages images)
{
var eligibleBackdrops = images.Backdrops == null ? new List<Backdrop>() :
images.Backdrops;
Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath),
CommunityRating = backdrop.VoteAverage,
VoteCount = backdrop.VoteCount,
Width = backdrop.Width,
Height = backdrop.Height,
ProviderName = Name,
Type = ImageType.Backdrop,
RatingType = RatingType.Score
});
}
return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
.ThenByDescending(i => i.Vote_Count);
return remoteImages.OrderByLanguageDescending(language);
}
public int Order => 0;
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);

@ -3,268 +3,116 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections;
using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
using MediaBrowser.Providers.Plugins.Tmdb.Movies;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
{
public class TmdbBoxSetProvider : IRemoteMetadataProvider<BoxSet, BoxSetInfo>
{
private const string GetCollectionInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/collection/{0}?api_key={1}&append_to_response=images";
internal static TmdbBoxSetProvider Current;
private readonly ILogger<TmdbBoxSetProvider> _logger;
private readonly IJsonSerializer _json;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
private readonly TmdbClientManager _tmdbClientManager;
public TmdbBoxSetProvider(
ILogger<TmdbBoxSetProvider> logger,
IJsonSerializer json,
IServerConfigurationManager config,
IFileSystem fileSystem,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager)
public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
_logger = logger;
_json = json;
_config = config;
_fileSystem = fileSystem;
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
Current = this;
_tmdbClientManager = tmdbClientManager;
}
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public string Name => TmdbUtils.ProviderName;
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo searchInfo, CancellationToken cancellationToken)
{
var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
var language = searchInfo.MetadataLanguage;
if (!string.IsNullOrEmpty(tmdbId))
if (tmdbId > 0)
{
await EnsureInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, searchInfo.MetadataLanguage);
var info = _json.DeserializeFromFile<CollectionResult>(dataFilePath);
var images = (info.Images ?? new CollectionImages()).Posters ?? new List<Poster>();
var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
if (collection == null)
{
return Enumerable.Empty<RemoteSearchResult>();
}
var result = new RemoteSearchResult
{
Name = info.Name,
SearchProviderName = Name,
ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path)
Name = collection.Name,
SearchProviderName = Name
};
result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
return new[] { result };
}
return await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
}
public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken)
{
var tmdbId = id.GetProviderId(MetadataProvider.Tmdb);
// We don't already have an Id, need to fetch it
if (string.IsNullOrEmpty(tmdbId))
{
var searchResults = await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(id, cancellationToken).ConfigureAwait(false);
var searchResult = searchResults.FirstOrDefault();
if (searchResult != null)
{
tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
}
}
var result = new MetadataResult<BoxSet>();
if (!string.IsNullOrEmpty(tmdbId))
{
var mainResult = await GetMovieDbResult(tmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (mainResult != null)
if (collection.Images != null)
{
result.HasMetadata = true;
result.Item = GetItem(mainResult);
result.ImageUrl = _tmdbClientManager.GetPosterUrl(collection.PosterPath);
}
}
return result;
}
result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
internal async Task<CollectionResult> GetMovieDbResult(string tmdbId, string language, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(tmdbId))
{
throw new ArgumentNullException(nameof(tmdbId));
return new[] { result };
}
await EnsureInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, language);
var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(dataFilePath))
var collections = new List<RemoteSearchResult>();
for (var i = 0; i < collectionSearchResults.Count; i++)
{
return _json.DeserializeFromFile<CollectionResult>(dataFilePath);
}
return null;
}
private BoxSet GetItem(CollectionResult obj)
{
var item = new BoxSet
{
Name = obj.Name,
Overview = obj.Overview
};
item.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
return item;
}
private async Task DownloadInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken)
{
var mainResult = await FetchMainResult(tmdbId, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
var collection = new RemoteSearchResult
{
Name = collectionSearchResults[i].Name,
SearchProviderName = Name
};
collection.SetProviderId(MetadataProvider.Tmdb, collectionSearchResults[i].Id.ToString(CultureInfo.InvariantCulture));
if (mainResult == null)
{
return;
collections.Add(collection);
}
var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage);
Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
_json.SerializeToFile(mainResult, dataFilePath);
return collections;
}
private async Task<CollectionResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken)
{
var url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey);
if (!string.IsNullOrEmpty(language))
var tmdbId = Convert.ToInt32(id.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
var language = id.MetadataLanguage;
// We don't already have an Id, need to fetch it
if (tmdbId <= 0)
{
url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
var searchResults = await _tmdbClientManager.SearchCollectionAsync(id.Name, language, cancellationToken).ConfigureAwait(false);
// Get images in english and with no language
url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
if (searchResults != null && searchResults.Count > 0)
{
tmdbId = searchResults[0].Id;
}
}
cancellationToken.ThrowIfCancellationRequested();
var result = new MetadataResult<BoxSet>();
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
foreach (var header in TmdbUtils.AcceptHeaders)
if (tmdbId > 0)
{
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
var mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(stream).ConfigureAwait(false);
var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
if (mainResult != null && string.IsNullOrEmpty(mainResult.Name))
{
if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
if (collection != null)
{
url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en";
if (!string.IsNullOrEmpty(language))
var item = new BoxSet
{
// Get images in english and with no language
url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
}
using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
foreach (var header in TmdbUtils.AcceptHeaders)
{
langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
}
await using var langStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(langStream).ConfigureAwait(false);
}
}
Name = collection.Name,
Overview = collection.Overview
};
return mainResult;
}
internal Task EnsureInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken)
{
var path = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage);
item.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
var fileInfo = _fileSystem.GetFileSystemInfo(path);
if (fileInfo.Exists)
{
// If it's recent or automatic updates are enabled, don't re-download
if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
{
return Task.CompletedTask;
result.HasMetadata = true;
result.Item = item;
}
}
return DownloadInfo(tmdbId, preferredMetadataLanguage, cancellationToken);
}
public string Name => TmdbUtils.ProviderName;
private static string GetDataFilePath(IApplicationPaths appPaths, string tmdbId, string preferredLanguage)
{
var path = GetDataPath(appPaths, tmdbId);
var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage ?? string.Empty);
return Path.Combine(path, filename);
}
private static string GetDataPath(IApplicationPaths appPaths, string tmdbId)
{
var dataPath = GetCollectionsDataPath(appPaths);
return Path.Combine(dataPath, tmdbId);
}
private static string GetCollectionsDataPath(IApplicationPaths appPaths)
{
var dataPath = Path.Combine(appPaths.CachePath, "tmdb-collections");
return dataPath;
return result;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)

@ -1,14 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
{
public class CollectionImages
{
public List<Backdrop> Backdrops { get; set; }
public List<Poster> Posters { get; set; }
}
}

@ -1,23 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
{
public class CollectionResult
{
public int Id { get; set; }
public string Name { get; set; }
public string Overview { get; set; }
public string Poster_Path { get; set; }
public string Backdrop_Path { get; set; }
public List<Part> Parts { get; set; }
public CollectionImages Images { get; set; }
}
}

@ -1,17 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
{
public class Part
{
public string Title { get; set; }
public int Id { get; set; }
public string Release_Date { get; set; }
public string Poster_Path { get; set; }
public string Backdrop_Path { get; set; }
}
}

@ -1,21 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Backdrop
{
public double Aspect_Ratio { get; set; }
public string File_Path { get; set; }
public int Height { get; set; }
public string Iso_639_1 { get; set; }
public double Vote_Average { get; set; }
public int Vote_Count { get; set; }
public int Width { get; set; }
}
}

@ -1,19 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Crew
{
public int Id { get; set; }
public string Credit_Id { get; set; }
public string Name { get; set; }
public string Department { get; set; }
public string Job { get; set; }
public string Profile_Path { get; set; }
}
}

@ -1,17 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class ExternalIds
{
public string Imdb_Id { get; set; }
public object Freebase_Id { get; set; }
public string Freebase_Mid { get; set; }
public int? Tvdb_Id { get; set; }
public int? Tvrage_Id { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Genre
{
public int Id { get; set; }
public string Name { get; set; }
}
}

@ -1,13 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Images
{
public List<Backdrop> Backdrops { get; set; }
public List<Poster> Posters { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Keyword
{
public int Id { get; set; }
public string Name { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Keywords
{
public List<Keyword> Results { get; set; }
}
}

@ -1,21 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Poster
{
public double Aspect_Ratio { get; set; }
public string File_Path { get; set; }
public int Height { get; set; }
public string Iso_639_1 { get; set; }
public double Vote_Average { get; set; }
public int Vote_Count { get; set; }
public int Width { get; set; }
}
}

@ -1,17 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Profile
{
public string File_Path { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public object Iso_639_1 { get; set; }
public double Aspect_Ratio { get; set; }
}
}

@ -1,23 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Still
{
public double Aspect_Ratio { get; set; }
public string File_Path { get; set; }
public int Height { get; set; }
public string Id { get; set; }
public string Iso_639_1 { get; set; }
public double Vote_Average { get; set; }
public int Vote_Count { get; set; }
public int Width { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class StillImages
{
public List<Still> Stills { get; set; }
}
}

@ -1,23 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Video
{
public string Id { get; set; }
public string Iso_639_1 { get; set; }
public string Iso_3166_1 { get; set; }
public string Key { get; set; }
public string Name { get; set; }
public string Site { get; set; }
public string Size { get; set; }
public string Type { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
{
public class Videos
{
public IReadOnlyList<Video> Results { get; set; }
}
}

@ -1,15 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class BelongsToCollection
{
public int Id { get; set; }
public string Name { get; set; }
public string Poster_Path { get; set; }
public string Backdrop_Path { get; set; }
}
}

@ -1,19 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class Cast
{
public int Id { get; set; }
public string Name { get; set; }
public string Character { get; set; }
public int Order { get; set; }
public int Cast_Id { get; set; }
public string Profile_Path { get; set; }
}
}

@ -1,14 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class Casts
{
public List<Cast> Cast { get; set; }
public List<Crew> Crew { get; set; }
}
}

@ -1,15 +0,0 @@
#pragma warning disable CS1591
using System;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class Country
{
public string Iso_3166_1 { get; set; }
public string Certification { get; set; }
public DateTime Release_Date { get; set; }
}
}

@ -1,80 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class MovieResult
{
public bool Adult { get; set; }
public string Backdrop_Path { get; set; }
public BelongsToCollection Belongs_To_Collection { get; set; }
public long Budget { get; set; }
public List<Genre> Genres { get; set; }
public string Homepage { get; set; }
public int Id { get; set; }
public string Imdb_Id { get; set; }
public string Original_Title { get; set; }
public string Original_Name { get; set; }
public string Overview { get; set; }
public double Popularity { get; set; }
public string Poster_Path { get; set; }
public List<ProductionCompany> Production_Companies { get; set; }
public List<ProductionCountry> Production_Countries { get; set; }
public string Release_Date { get; set; }
public long Revenue { get; set; }
public int Runtime { get; set; }
public List<SpokenLanguage> Spoken_Languages { get; set; }
public string Status { get; set; }
public string Tagline { get; set; }
public string Title { get; set; }
public string Name { get; set; }
public double Vote_Average { get; set; }
public int Vote_Count { get; set; }
public Casts Casts { get; set; }
public Releases Releases { get; set; }
public Images Images { get; set; }
public Keywords Keywords { get; set; }
public Trailers Trailers { get; set; }
public string GetOriginalTitle()
{
return Original_Name ?? Original_Title;
}
public string GetTitle()
{
return Name ?? Title ?? GetOriginalTitle();
}
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class ProductionCompany
{
public string Name { get; set; }
public int Id { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class ProductionCountry
{
public string Iso_3166_1 { get; set; }
public string Name { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class Releases
{
public List<Country> Countries { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class SpokenLanguage
{
public string Iso_639_1 { get; set; }
public string Name { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class Trailers
{
public IReadOnlyList<Youtube> Youtube { get; set; }
}
}

@ -1,13 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
{
public class Youtube
{
public string Name { get; set; }
public string Size { get; set; }
public string Source { get; set; }
}
}

@ -1,12 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People
{
public class PersonImages
{
public IReadOnlyList<Profile> Profiles { get; set; }
}
}

@ -1,38 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People
{
public class PersonResult
{
public bool Adult { get; set; }
public List<string> Also_Known_As { get; set; }
public string Biography { get; set; }
public string Birthday { get; set; }
public string Deathday { get; set; }
public string Homepage { get; set; }
public int Id { get; set; }
public string Imdb_Id { get; set; }
public string Name { get; set; }
public string Place_Of_Birth { get; set; }
public double Popularity { get; set; }
public string Profile_Path { get; set; }
public PersonImages Images { get; set; }
public ExternalIds External_Ids { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
{
public class ExternalIdLookupResult
{
public List<TvResult> Tv_Results { get; set; }
}
}

@ -1,78 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
{
public class MovieResult
{
/// <summary>
/// Gets or sets a value indicating whether this <see cref="MovieResult" /> is adult.
/// </summary>
/// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
public bool Adult { get; set; }
/// <summary>
/// Gets or sets the backdrop_path.
/// </summary>
/// <value>The backdrop_path.</value>
public string Backdrop_Path { get; set; }
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
public int Id { get; set; }
/// <summary>
/// Gets or sets the original_title.
/// </summary>
/// <value>The original_title.</value>
public string Original_Title { get; set; }
/// <summary>
/// Gets or sets the original_name.
/// </summary>
/// <value>The original_name.</value>
public string Original_Name { get; set; }
/// <summary>
/// Gets or sets the release_date.
/// </summary>
/// <value>The release_date.</value>
public string Release_Date { get; set; }
/// <summary>
/// Gets or sets the poster_path.
/// </summary>
/// <value>The poster_path.</value>
public string Poster_Path { get; set; }
/// <summary>
/// Gets or sets the popularity.
/// </summary>
/// <value>The popularity.</value>
public double Popularity { get; set; }
/// <summary>
/// Gets or sets the title.
/// </summary>
/// <value>The title.</value>
public string Title { get; set; }
/// <summary>
/// Gets or sets the vote_average.
/// </summary>
/// <value>The vote_average.</value>
public double Vote_Average { get; set; }
/// <summary>
/// For collection search results.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the vote_count.
/// </summary>
/// <value>The vote_count.</value>
public int Vote_Count { get; set; }
}
}

@ -1,31 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
{
public class PersonSearchResult
{
/// <summary>
/// Gets or sets a value indicating whether this <see cref="PersonSearchResult" /> is adult.
/// </summary>
/// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
public bool Adult { get; set; }
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
public int Id { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the profile_ path.
/// </summary>
/// <value>The profile_ path.</value>
public string Profile_Path { get; set; }
}
}

@ -1,33 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
{
public class TmdbSearchResult<T>
{
/// <summary>
/// Gets or sets the page.
/// </summary>
/// <value>The page.</value>
public int Page { get; set; }
/// <summary>
/// Gets or sets the results.
/// </summary>
/// <value>The results.</value>
public List<T> Results { get; set; }
/// <summary>
/// Gets or sets the total_pages.
/// </summary>
/// <value>The total_pages.</value>
public int Total_Pages { get; set; }
/// <summary>
/// Gets or sets the total_results.
/// </summary>
/// <value>The total_results.</value>
public int Total_Results { get; set; }
}
}

@ -1,25 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
{
public class TvResult
{
public string Backdrop_Path { get; set; }
public string First_Air_Date { get; set; }
public int Id { get; set; }
public string Original_Name { get; set; }
public string Poster_Path { get; set; }
public double Popularity { get; set; }
public string Name { get; set; }
public double Vote_Average { get; set; }
public int Vote_Count { get; set; }
}
}

@ -1,19 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
{
public class Cast
{
public string Character { get; set; }
public string Credit_Id { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public string Profile_Path { get; set; }
public int Order { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
{
public class ContentRating
{
public string Iso_3166_1 { get; set; }
public string Rating { get; set; }
}
}

@ -1,11 +0,0 @@
#pragma warning disable CS1591
using System.Collections.Generic;
namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
{
public class ContentRatings
{
public List<ContentRating> Results { get; set; }
}
}

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

Loading…
Cancel
Save