commit
3ddbda9aca
@ -0,0 +1,183 @@
|
||||
name: $(Date:yyyyMMdd)$(Rev:.r)
|
||||
|
||||
variables:
|
||||
- name: TestProjects
|
||||
value: 'Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj'
|
||||
- name: RestoreBuildProjects
|
||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||
|
||||
pr:
|
||||
autoCancel: true
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
- job: main_build
|
||||
displayName: Main Build
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
strategy:
|
||||
matrix:
|
||||
release:
|
||||
BuildConfiguration: Release
|
||||
debug:
|
||||
BuildConfiguration: Debug
|
||||
maxParallel: 2
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
submodules: true
|
||||
persistCredentials: false
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Restore
|
||||
inputs:
|
||||
command: restore
|
||||
projects: '$(RestoreBuildProjects)'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Build
|
||||
inputs:
|
||||
projects: '$(RestoreBuildProjects)'
|
||||
arguments: '--configuration $(BuildConfiguration)'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Test
|
||||
inputs:
|
||||
command: test
|
||||
projects: '$(RestoreBuildProjects)'
|
||||
arguments: '--configuration $(BuildConfiguration)'
|
||||
enabled: false
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Publish
|
||||
inputs:
|
||||
command: publish
|
||||
publishWebProjects: false
|
||||
projects: '$(RestoreBuildProjects)'
|
||||
arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)'
|
||||
zipAfterPublish: false
|
||||
|
||||
# - task: PublishBuildArtifacts@1
|
||||
# displayName: 'Publish Artifact'
|
||||
# inputs:
|
||||
# PathtoPublish: '$(build.artifactstagingdirectory)'
|
||||
# artifactName: 'jellyfin-build-$(BuildConfiguration)'
|
||||
# zipAfterPublish: true
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact Naming'
|
||||
condition: eq(variables['BuildConfiguration'], 'Release')
|
||||
inputs:
|
||||
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
|
||||
artifactName: 'Jellyfin.Naming'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact Controller'
|
||||
condition: eq(variables['BuildConfiguration'], 'Release')
|
||||
inputs:
|
||||
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
|
||||
artifactName: 'Jellyfin.Controller'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact Model'
|
||||
condition: eq(variables['BuildConfiguration'], 'Release')
|
||||
inputs:
|
||||
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
|
||||
artifactName: 'Jellyfin.Model'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact Common'
|
||||
condition: eq(variables['BuildConfiguration'], 'Release')
|
||||
inputs:
|
||||
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
|
||||
artifactName: 'Jellyfin.Common'
|
||||
|
||||
- job: dotnet_compat
|
||||
displayName: Compatibility Check
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
dependsOn: main_build
|
||||
condition: succeeded()
|
||||
strategy:
|
||||
matrix:
|
||||
Naming:
|
||||
NugetPackageName: Jellyfin.Naming
|
||||
AssemblyFileName: Emby.Naming.dll
|
||||
Controller:
|
||||
NugetPackageName: Jellyfin.Controller
|
||||
AssemblyFileName: MediaBrowser.Controller.dll
|
||||
Model:
|
||||
NugetPackageName: Jellyfin.Model
|
||||
AssemblyFileName: MediaBrowser.Model.dll
|
||||
Common:
|
||||
NugetPackageName: Jellyfin.Common
|
||||
AssemblyFileName: MediaBrowser.Common.dll
|
||||
maxParallel: 2
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Download $(NugetPackageName)'
|
||||
inputs:
|
||||
command: custom
|
||||
arguments: 'install $(NugetPackageName) -OutputDirectory $(System.ArtifactsDirectory)/packages -ExcludeVersion -DirectDownload'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy Nuget Assembly to current-release folder
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/packages/$(NugetPackageName) # Optional
|
||||
contents: '**/*.dll'
|
||||
targetFolder: $(System.ArtifactsDirectory)/current-release
|
||||
cleanTargetFolder: true # Optional
|
||||
overWrite: true # Optional
|
||||
flattenFolders: true # Optional
|
||||
|
||||
- task: DownloadBuildArtifacts@0
|
||||
displayName: Download the Assembly Build Artifact
|
||||
inputs:
|
||||
buildType: 'current' # Options: current, specific
|
||||
allowPartiallySucceededBuilds: false # Optional
|
||||
downloadType: 'single' # Options: single, specific
|
||||
artifactName: '$(NugetPackageName)' # Required when downloadType == Single
|
||||
downloadPath: '$(System.ArtifactsDirectory)/new-artifacts'
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy Artifact Assembly to new-release folder
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
|
||||
contents: '**/*.dll'
|
||||
targetFolder: $(System.ArtifactsDirectory)/new-release
|
||||
cleanTargetFolder: true # Optional
|
||||
overWrite: true # Optional
|
||||
flattenFolders: true # Optional
|
||||
|
||||
- task: DownloadGitHubRelease@0
|
||||
displayName: Download ABI compatibility check tool from GitHub
|
||||
inputs:
|
||||
connection: Jellyfin GitHub
|
||||
userRepository: EraYaN/dotnet-compatibility
|
||||
defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag
|
||||
#version: # Required when defaultVersionType != Latest
|
||||
itemPattern: '**-ci.zip' # Optional
|
||||
downloadPath: '$(System.ArtifactsDirectory)'
|
||||
|
||||
- task: ExtractFiles@1
|
||||
displayName: Extract ABI compatibility check tool
|
||||
inputs:
|
||||
archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip'
|
||||
destinationFolder: $(System.ArtifactsDirectory)/tools
|
||||
cleanDestinationFolder: true
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: Execute ABI compatibility check tool
|
||||
inputs:
|
||||
script: 'dotnet tools/CompatibilityCheckerCoreCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName)'
|
||||
workingDirectory: $(System.ArtifactsDirectory) # Optional
|
||||
#failOnStderr: false # Optional
|
||||
|
||||
|
@ -1,36 +0,0 @@
|
||||
namespace DvdLib.Ifo
|
||||
{
|
||||
public enum AudioCodec
|
||||
{
|
||||
AC3 = 0,
|
||||
MPEG1 = 2,
|
||||
MPEG2ext = 3,
|
||||
LPCM = 4,
|
||||
DTS = 6,
|
||||
}
|
||||
|
||||
public enum ApplicationMode
|
||||
{
|
||||
Unspecified = 0,
|
||||
Karaoke = 1,
|
||||
Surround = 2,
|
||||
}
|
||||
|
||||
public class AudioAttributes
|
||||
{
|
||||
public readonly AudioCodec Codec;
|
||||
public readonly bool MultichannelExtensionPresent;
|
||||
public readonly ApplicationMode Mode;
|
||||
public readonly byte QuantDRC;
|
||||
public readonly byte SampleRate;
|
||||
public readonly byte Channels;
|
||||
public readonly ushort LanguageCode;
|
||||
public readonly byte LanguageExtension;
|
||||
public readonly byte CodeExtension;
|
||||
}
|
||||
|
||||
public class MultiChannelExtension
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DvdLib.Ifo
|
||||
{
|
||||
public class ProgramChainCommandTable
|
||||
{
|
||||
public readonly ushort LastByteAddress;
|
||||
public readonly List<VirtualMachineCommand> PreCommands;
|
||||
public readonly List<VirtualMachineCommand> PostCommands;
|
||||
public readonly List<VirtualMachineCommand> CellCommands;
|
||||
}
|
||||
|
||||
public class VirtualMachineCommand
|
||||
{
|
||||
public readonly byte[] Command;
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
namespace DvdLib.Ifo
|
||||
{
|
||||
public enum VideoCodec
|
||||
{
|
||||
MPEG1 = 0,
|
||||
MPEG2 = 1,
|
||||
}
|
||||
|
||||
public enum VideoFormat
|
||||
{
|
||||
NTSC = 0,
|
||||
PAL = 1,
|
||||
}
|
||||
|
||||
public enum AspectRatio
|
||||
{
|
||||
ar4to3 = 0,
|
||||
ar16to9 = 3
|
||||
}
|
||||
|
||||
public enum FilmMode
|
||||
{
|
||||
None = -1,
|
||||
Camera = 0,
|
||||
Film = 1,
|
||||
}
|
||||
|
||||
public class VideoAttributes
|
||||
{
|
||||
public readonly VideoCodec Codec;
|
||||
public readonly VideoFormat Format;
|
||||
public readonly AspectRatio Aspect;
|
||||
public readonly bool AutomaticPanScan;
|
||||
public readonly bool AutomaticLetterBox;
|
||||
public readonly bool Line21CCField1;
|
||||
public readonly bool Line21CCField2;
|
||||
public readonly int Width;
|
||||
public readonly int Height;
|
||||
public readonly bool Letterboxed;
|
||||
public readonly FilmMode FilmMode;
|
||||
|
||||
public VideoAttributes()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class CurrentIdEventArgs : EventArgs
|
||||
{
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class TransportStateEventArgs : EventArgs
|
||||
{
|
||||
public TRANSPORTSTATE State { get; set; }
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class uParser
|
||||
{
|
||||
public static IList<uBaseObject> ParseBrowseXml(XDocument doc)
|
||||
{
|
||||
if (doc == null)
|
||||
{
|
||||
throw new ArgumentException("doc");
|
||||
}
|
||||
|
||||
var list = new List<uBaseObject>();
|
||||
|
||||
var document = doc.Document;
|
||||
|
||||
if (document == null)
|
||||
return list;
|
||||
|
||||
var item = (from result in document.Descendants("Result") select result).FirstOrDefault();
|
||||
|
||||
if (item == null)
|
||||
return list;
|
||||
|
||||
var uPnpResponse = XElement.Parse((string)item);
|
||||
|
||||
var uObjects = from container in uPnpResponse.Elements(uPnpNamespaces.containers)
|
||||
select new uParserObject { Element = container };
|
||||
|
||||
var uObjects2 = from container in uPnpResponse.Elements(uPnpNamespaces.items)
|
||||
select new uParserObject { Element = container };
|
||||
|
||||
list.AddRange(uObjects.Concat(uObjects2).Select(CreateObjectFromXML).Where(uObject => uObject != null));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static uBaseObject CreateObjectFromXML(uParserObject uItem)
|
||||
{
|
||||
return UpnpContainer.Create(uItem.Element);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class uParserObject
|
||||
{
|
||||
public XElement Element { get; set; }
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
namespace Emby.Server.Implementations.FFMpeg
|
||||
{
|
||||
/// <summary>
|
||||
/// Class FFMpegInfo
|
||||
/// </summary>
|
||||
public class FFMpegInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the path.
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public string EncoderPath { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the probe path.
|
||||
/// </summary>
|
||||
/// <value>The probe path.</value>
|
||||
public string ProbePath { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the version.
|
||||
/// </summary>
|
||||
/// <value>The version.</value>
|
||||
public string Version { get; set; }
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
namespace Emby.Server.Implementations.FFMpeg
|
||||
{
|
||||
public class FFMpegInstallInfo
|
||||
{
|
||||
public string Version { get; set; }
|
||||
public string FFMpegFilename { get; set; }
|
||||
public string FFProbeFilename { get; set; }
|
||||
public string ArchiveType { get; set; }
|
||||
|
||||
public FFMpegInstallInfo()
|
||||
{
|
||||
Version = "Path";
|
||||
FFMpegFilename = "ffmpeg";
|
||||
FFProbeFilename = "ffprobe";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Emby.Server.Implementations.FFMpeg
|
||||
{
|
||||
public class FFMpegLoader
|
||||
{
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly FFMpegInstallInfo _ffmpegInstallInfo;
|
||||
|
||||
public FFMpegLoader(IApplicationPaths appPaths, IFileSystem fileSystem, FFMpegInstallInfo ffmpegInstallInfo)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_ffmpegInstallInfo = ffmpegInstallInfo;
|
||||
}
|
||||
|
||||
public FFMpegInfo GetFFMpegInfo(IStartupOptions options)
|
||||
{
|
||||
var customffMpegPath = options.FFmpegPath;
|
||||
var customffProbePath = options.FFprobePath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(customffMpegPath) && !string.IsNullOrWhiteSpace(customffProbePath))
|
||||
{
|
||||
return new FFMpegInfo
|
||||
{
|
||||
ProbePath = customffProbePath,
|
||||
EncoderPath = customffMpegPath,
|
||||
Version = "external"
|
||||
};
|
||||
}
|
||||
|
||||
var downloadInfo = _ffmpegInstallInfo;
|
||||
|
||||
var prebuiltFolder = _appPaths.ProgramSystemPath;
|
||||
var prebuiltffmpeg = Path.Combine(prebuiltFolder, downloadInfo.FFMpegFilename);
|
||||
var prebuiltffprobe = Path.Combine(prebuiltFolder, downloadInfo.FFProbeFilename);
|
||||
if (File.Exists(prebuiltffmpeg) && File.Exists(prebuiltffprobe))
|
||||
{
|
||||
return new FFMpegInfo
|
||||
{
|
||||
ProbePath = prebuiltffprobe,
|
||||
EncoderPath = prebuiltffmpeg,
|
||||
Version = "external"
|
||||
};
|
||||
}
|
||||
|
||||
var version = downloadInfo.Version;
|
||||
|
||||
if (string.Equals(version, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new FFMpegInfo();
|
||||
}
|
||||
|
||||
var rootEncoderPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
|
||||
var versionedDirectoryPath = Path.Combine(rootEncoderPath, version);
|
||||
|
||||
var info = new FFMpegInfo
|
||||
{
|
||||
ProbePath = Path.Combine(versionedDirectoryPath, downloadInfo.FFProbeFilename),
|
||||
EncoderPath = Path.Combine(versionedDirectoryPath, downloadInfo.FFMpegFilename),
|
||||
Version = version
|
||||
};
|
||||
|
||||
Directory.CreateDirectory(versionedDirectoryPath);
|
||||
|
||||
var excludeFromDeletions = new List<string> { versionedDirectoryPath };
|
||||
|
||||
if (!File.Exists(info.ProbePath) || !File.Exists(info.EncoderPath))
|
||||
{
|
||||
// ffmpeg not present. See if there's an older version we can start with
|
||||
var existingVersion = GetExistingVersion(info, rootEncoderPath);
|
||||
|
||||
// No older version. Need to download and block until complete
|
||||
if (existingVersion == null)
|
||||
{
|
||||
return new FFMpegInfo();
|
||||
}
|
||||
else
|
||||
{
|
||||
info = existingVersion;
|
||||
versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath);
|
||||
excludeFromDeletions.Add(versionedDirectoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow just one of these to be overridden, if desired.
|
||||
if (!string.IsNullOrWhiteSpace(customffMpegPath))
|
||||
{
|
||||
info.EncoderPath = customffMpegPath;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(customffProbePath))
|
||||
{
|
||||
info.ProbePath = customffProbePath;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private FFMpegInfo GetExistingVersion(FFMpegInfo info, string rootEncoderPath)
|
||||
{
|
||||
var encoderFilename = Path.GetFileName(info.EncoderPath);
|
||||
var probeFilename = Path.GetFileName(info.ProbePath);
|
||||
|
||||
foreach (var directory in _fileSystem.GetDirectoryPaths(rootEncoderPath))
|
||||
{
|
||||
var allFiles = _fileSystem.GetFilePaths(directory, true).ToList();
|
||||
|
||||
var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase));
|
||||
var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(encoder) &&
|
||||
!string.IsNullOrWhiteSpace(probe))
|
||||
{
|
||||
return new FFMpegInfo
|
||||
{
|
||||
EncoderPath = encoder,
|
||||
ProbePath = probe,
|
||||
Version = Path.GetFileName(Path.GetDirectoryName(probe))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,97 +1,97 @@
|
||||
{
|
||||
"Albums": "Albums",
|
||||
"AppDeviceValues": "App: {0}, Device: {1}",
|
||||
"Application": "Application",
|
||||
"Artists": "Artists",
|
||||
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
|
||||
"Books": "Books",
|
||||
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
||||
"Channels": "Channels",
|
||||
"ChapterNameValue": "Chapter {0}",
|
||||
"Collections": "Collections",
|
||||
"DeviceOfflineWithName": "{0} has disconnected",
|
||||
"DeviceOnlineWithName": "{0} is connected",
|
||||
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
|
||||
"Favorites": "Favorites",
|
||||
"Folders": "Folders",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Album Artists",
|
||||
"HeaderCameraUploads": "Camera Uploads",
|
||||
"HeaderContinueWatching": "Continue Watching",
|
||||
"HeaderFavoriteAlbums": "Favorite Albums",
|
||||
"HeaderFavoriteArtists": "Favorite Artists",
|
||||
"HeaderFavoriteEpisodes": "Favorite Episodes",
|
||||
"HeaderFavoriteShows": "Favorite Shows",
|
||||
"HeaderFavoriteSongs": "Favorite Songs",
|
||||
"HeaderLiveTV": "Live TV",
|
||||
"HeaderNextUp": "Next Up",
|
||||
"HeaderRecordingGroups": "Recording Groups",
|
||||
"HomeVideos": "Home videos",
|
||||
"Inherit": "Inherit",
|
||||
"ItemAddedWithName": "{0} was added to the library",
|
||||
"ItemRemovedWithName": "{0} was removed from the library",
|
||||
"LabelIpAddressValue": "Ip address: {0}",
|
||||
"LabelRunningTimeValue": "Running time: {0}",
|
||||
"Latest": "Latest",
|
||||
"MessageApplicationUpdated": "Jellyfin Server has been updated",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
|
||||
"MessageServerConfigurationUpdated": "Server configuration has been updated",
|
||||
"MixedContent": "Mixed content",
|
||||
"Movies": "Movies",
|
||||
"Music": "Music",
|
||||
"MusicVideos": "Music videos",
|
||||
"NameInstallFailed": "{0} installation failed",
|
||||
"NameSeasonNumber": "Season {0}",
|
||||
"NameSeasonUnknown": "Season Unknown",
|
||||
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Application update available",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
||||
"NotificationOptionAudioPlayback": "Audio playback started",
|
||||
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
|
||||
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
|
||||
"NotificationOptionInstallationFailed": "Installation failure",
|
||||
"NotificationOptionNewLibraryContent": "New content added",
|
||||
"NotificationOptionPluginError": "Plugin failure",
|
||||
"NotificationOptionPluginInstalled": "Plugin installed",
|
||||
"NotificationOptionPluginUninstalled": "Plugin uninstalled",
|
||||
"NotificationOptionPluginUpdateInstalled": "Plugin update installed",
|
||||
"NotificationOptionServerRestartRequired": "Server restart required",
|
||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
||||
"NotificationOptionUserLockedOut": "User locked out",
|
||||
"NotificationOptionVideoPlayback": "Video playback started",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
|
||||
"Photos": "Photos",
|
||||
"Playlists": "Playlists",
|
||||
"Albums": "Álbumes",
|
||||
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
|
||||
"Application": "Aplicación",
|
||||
"Artists": "Artistas",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
|
||||
"Books": "Libros",
|
||||
"CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
|
||||
"Channels": "Canales",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"Collections": "Colecciones",
|
||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||
"DeviceOnlineWithName": "{0} está conectado",
|
||||
"FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}",
|
||||
"Favorites": "Favoritos",
|
||||
"Folders": "Carpetas",
|
||||
"Genres": "Géneros",
|
||||
"HeaderAlbumArtists": "Artistas de álbumes",
|
||||
"HeaderCameraUploads": "Subidas de cámara",
|
||||
"HeaderContinueWatching": "Continuar viendo",
|
||||
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||
"HeaderFavoriteEpisodes": "Episodios favoritos",
|
||||
"HeaderFavoriteShows": "Programas favoritos",
|
||||
"HeaderFavoriteSongs": "Canciones favoritas",
|
||||
"HeaderLiveTV": "TV en vivo",
|
||||
"HeaderNextUp": "Continuar Viendo",
|
||||
"HeaderRecordingGroups": "Grupos de grabación",
|
||||
"HomeVideos": "Videos caseros",
|
||||
"Inherit": "Heredar",
|
||||
"ItemAddedWithName": "{0} se ha añadido a la biblioteca",
|
||||
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
|
||||
"LabelIpAddressValue": "Dirección IP: {0}",
|
||||
"LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
|
||||
"Latest": "Últimos",
|
||||
"MessageApplicationUpdated": "El servidor Jellyfin fue actualizado",
|
||||
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Fue actualizada la sección {0} de la configuración del servidor",
|
||||
"MessageServerConfigurationUpdated": "Fue actualizada la configuración del servidor",
|
||||
"MixedContent": "Contenido mixto",
|
||||
"Movies": "Películas",
|
||||
"Music": "Música",
|
||||
"MusicVideos": "Videos musicales",
|
||||
"NameInstallFailed": "{0} error de instalación",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameSeasonUnknown": "Temporada desconocida",
|
||||
"NewVersionIsAvailable": "Disponible una nueva versión de Jellyfin para descargar.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada",
|
||||
"NotificationOptionAudioPlayback": "Se inició la reproducción de audio",
|
||||
"NotificationOptionAudioPlaybackStopped": "Se detuvo la reproducción de audio",
|
||||
"NotificationOptionCameraImageUploaded": "Imagen de la cámara cargada",
|
||||
"NotificationOptionInstallationFailed": "Error de instalación",
|
||||
"NotificationOptionNewLibraryContent": "Nuevo contenido añadido",
|
||||
"NotificationOptionPluginError": "Error en plugin",
|
||||
"NotificationOptionPluginInstalled": "Plugin instalado",
|
||||
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
|
||||
"NotificationOptionPluginUpdateInstalled": "Actualización del complemento instalada",
|
||||
"NotificationOptionServerRestartRequired": "Se requiere reinicio del servidor",
|
||||
"NotificationOptionTaskFailed": "Error de tarea programada",
|
||||
"NotificationOptionUserLockedOut": "Usuario bloqueado",
|
||||
"NotificationOptionVideoPlayback": "Se inició la reproducción de video",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
|
||||
"Photos": "Fotos",
|
||||
"Playlists": "Listas de reproducción",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0} was installed",
|
||||
"PluginUninstalledWithName": "{0} was uninstalled",
|
||||
"PluginUpdatedWithName": "{0} was updated",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} failed",
|
||||
"ScheduledTaskStartedWithName": "{0} started",
|
||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
||||
"PluginInstalledWithName": "{0} fue instalado",
|
||||
"PluginUninstalledWithName": "{0} fue desinstalado",
|
||||
"PluginUpdatedWithName": "{0} fue actualizado",
|
||||
"ProviderValue": "Proveedor: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} falló",
|
||||
"ScheduledTaskStartedWithName": "{0} iniciada",
|
||||
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
|
||||
"Shows": "Series",
|
||||
"Songs": "Songs",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
|
||||
"Songs": "Canciones",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
||||
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
|
||||
"Sync": "Sync",
|
||||
"System": "System",
|
||||
"TvShows": "TV Shows",
|
||||
"User": "User",
|
||||
"UserCreatedWithName": "User {0} has been created",
|
||||
"UserDeletedWithName": "User {0} has been deleted",
|
||||
"UserDownloadingItemWithValues": "{0} is downloading {1}",
|
||||
"UserLockedOutWithName": "User {0} has been locked out",
|
||||
"UserOfflineFromDevice": "{0} has disconnected from {1}",
|
||||
"UserOnlineFromDevice": "{0} is online from {1}",
|
||||
"UserPasswordChangedWithName": "Password has been changed for user {0}",
|
||||
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
||||
"ValueSpecialEpisodeName": "Special - {0}",
|
||||
"VersionNumber": "Version {0}"
|
||||
"SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
|
||||
"SubtitlesDownloadedForItem": "Descargar subtítulos para {0}",
|
||||
"Sync": "Sincronizar",
|
||||
"System": "Sistema",
|
||||
"TvShows": "Series de TV",
|
||||
"User": "Usuario",
|
||||
"UserCreatedWithName": "El usuario {0} ha sido creado",
|
||||
"UserDeletedWithName": "El usuario {0} ha sido borrado",
|
||||
"UserDownloadingItemWithValues": "{0} está descargando {1}",
|
||||
"UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
|
||||
"UserOfflineFromDevice": "{0} se ha desconectado de {1}",
|
||||
"UserOnlineFromDevice": "{0} está en línea desde {1}",
|
||||
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
|
||||
"UserPolicyUpdatedWithName": "Actualizada política de usuario para {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ha sido añadido a tu biblioteca multimedia",
|
||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||
"VersionNumber": "Versión {0}"
|
||||
}
|
||||
|
@ -1,97 +1,97 @@
|
||||
{
|
||||
"Albums": "Albums",
|
||||
"AppDeviceValues": "App: {0}, Device: {1}",
|
||||
"AppDeviceValues": "Application : {0}, Appareil : {1}",
|
||||
"Application": "Application",
|
||||
"Artists": "Artists",
|
||||
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
|
||||
"Books": "Books",
|
||||
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
|
||||
"Channels": "Channels",
|
||||
"ChapterNameValue": "Chapter {0}",
|
||||
"Artists": "Artistes",
|
||||
"AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès",
|
||||
"Books": "Livres",
|
||||
"CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}",
|
||||
"Channels": "Chaînes",
|
||||
"ChapterNameValue": "Chapitre {0}",
|
||||
"Collections": "Collections",
|
||||
"DeviceOfflineWithName": "{0} has disconnected",
|
||||
"DeviceOnlineWithName": "{0} is connected",
|
||||
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
|
||||
"Favorites": "Favorites",
|
||||
"Folders": "Folders",
|
||||
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
||||
"DeviceOnlineWithName": "{0} est connecté",
|
||||
"FailedLoginAttemptWithUserName": "Échec d'une tentative de connexion de {0}",
|
||||
"Favorites": "Favoris",
|
||||
"Folders": "Dossiers",
|
||||
"Genres": "Genres",
|
||||
"HeaderAlbumArtists": "Album Artists",
|
||||
"HeaderCameraUploads": "Camera Uploads",
|
||||
"HeaderAlbumArtists": "Artistes de l'album",
|
||||
"HeaderCameraUploads": "Photos transférées",
|
||||
"HeaderContinueWatching": "Continuer à regarder",
|
||||
"HeaderFavoriteAlbums": "Favorite Albums",
|
||||
"HeaderFavoriteArtists": "Favorite Artists",
|
||||
"HeaderFavoriteEpisodes": "Favorite Episodes",
|
||||
"HeaderFavoriteShows": "Favorite Shows",
|
||||
"HeaderFavoriteSongs": "Favorite Songs",
|
||||
"HeaderLiveTV": "Live TV",
|
||||
"HeaderFavoriteAlbums": "Albums favoris",
|
||||
"HeaderFavoriteArtists": "Artistes favoris",
|
||||
"HeaderFavoriteEpisodes": "Épisodes favoris",
|
||||
"HeaderFavoriteShows": "Séries favorites",
|
||||
"HeaderFavoriteSongs": "Chansons favorites",
|
||||
"HeaderLiveTV": "TV en direct",
|
||||
"HeaderNextUp": "À Suivre",
|
||||
"HeaderRecordingGroups": "Recording Groups",
|
||||
"HomeVideos": "Home videos",
|
||||
"Inherit": "Inherit",
|
||||
"ItemAddedWithName": "{0} was added to the library",
|
||||
"ItemRemovedWithName": "{0} was removed from the library",
|
||||
"LabelIpAddressValue": "Ip address: {0}",
|
||||
"LabelRunningTimeValue": "Running time: {0}",
|
||||
"Latest": "Latest",
|
||||
"MessageApplicationUpdated": "Jellyfin Server has been updated",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
|
||||
"MessageServerConfigurationUpdated": "Server configuration has been updated",
|
||||
"MixedContent": "Mixed content",
|
||||
"Movies": "Movies",
|
||||
"Music": "Music",
|
||||
"MusicVideos": "Music videos",
|
||||
"NameInstallFailed": "{0} installation failed",
|
||||
"NameSeasonNumber": "Season {0}",
|
||||
"NameSeasonUnknown": "Season Unknown",
|
||||
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Application update available",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
||||
"NotificationOptionAudioPlayback": "Audio playback started",
|
||||
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
|
||||
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
|
||||
"NotificationOptionInstallationFailed": "Installation failure",
|
||||
"NotificationOptionNewLibraryContent": "New content added",
|
||||
"NotificationOptionPluginError": "Plugin failure",
|
||||
"NotificationOptionPluginInstalled": "Plugin installed",
|
||||
"NotificationOptionPluginUninstalled": "Plugin uninstalled",
|
||||
"NotificationOptionPluginUpdateInstalled": "Plugin update installed",
|
||||
"NotificationOptionServerRestartRequired": "Server restart required",
|
||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
||||
"NotificationOptionUserLockedOut": "User locked out",
|
||||
"NotificationOptionVideoPlayback": "Video playback started",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
|
||||
"HeaderRecordingGroups": "Groupes d'enregistrements",
|
||||
"HomeVideos": "Vidéos personnelles",
|
||||
"Inherit": "Hériter",
|
||||
"ItemAddedWithName": "{0} a été ajouté à la médiathèque",
|
||||
"ItemRemovedWithName": "{0} a été supprimé de la médiathèque",
|
||||
"LabelIpAddressValue": "Adresse IP : {0}",
|
||||
"LabelRunningTimeValue": "Durée : {0}",
|
||||
"Latest": "Derniers",
|
||||
"MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
|
||||
"MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers la version {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
|
||||
"MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour",
|
||||
"MixedContent": "Contenu mixte",
|
||||
"Movies": "Films",
|
||||
"Music": "Musique",
|
||||
"MusicVideos": "Vidéos musicales",
|
||||
"NameInstallFailed": "{0} échec d'installation",
|
||||
"NameSeasonNumber": "Saison {0}",
|
||||
"NameSeasonUnknown": "Saison Inconnue",
|
||||
"NewVersionIsAvailable": "Une nouvelle version du serveur Jellyfin est disponible au téléchargement.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
|
||||
"NotificationOptionAudioPlayback": "Lecture audio démarrée",
|
||||
"NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée",
|
||||
"NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée",
|
||||
"NotificationOptionInstallationFailed": "Échec d'installation",
|
||||
"NotificationOptionNewLibraryContent": "Nouveau contenu ajouté",
|
||||
"NotificationOptionPluginError": "Erreur d'extension",
|
||||
"NotificationOptionPluginInstalled": "Extension installée",
|
||||
"NotificationOptionPluginUninstalled": "Extension désinstallée",
|
||||
"NotificationOptionPluginUpdateInstalled": "Mise à jour d'extension installée",
|
||||
"NotificationOptionServerRestartRequired": "Un redémarrage du serveur est requis",
|
||||
"NotificationOptionTaskFailed": "Échec de tâche planifiée",
|
||||
"NotificationOptionUserLockedOut": "Utilisateur verrouillé",
|
||||
"NotificationOptionVideoPlayback": "Lecture vidéo démarrée",
|
||||
"NotificationOptionVideoPlaybackStopped": "Lecture vidéo arrêtée",
|
||||
"Photos": "Photos",
|
||||
"Playlists": "Playlists",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0} was installed",
|
||||
"PluginUninstalledWithName": "{0} was uninstalled",
|
||||
"PluginUpdatedWithName": "{0} was updated",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} failed",
|
||||
"ScheduledTaskStartedWithName": "{0} started",
|
||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
||||
"Shows": "Series",
|
||||
"Songs": "Songs",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
|
||||
"Playlists": "Listes de lecture",
|
||||
"Plugin": "Extension",
|
||||
"PluginInstalledWithName": "{0} a été installé",
|
||||
"PluginUninstalledWithName": "{0} a été désinstallé",
|
||||
"PluginUpdatedWithName": "{0} a été mis à jour",
|
||||
"ProviderValue": "Fournisseur : {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} a échoué",
|
||||
"ScheduledTaskStartedWithName": "{0} a commencé",
|
||||
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
|
||||
"Shows": "Émissions",
|
||||
"Songs": "Chansons",
|
||||
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
||||
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
|
||||
"Sync": "Sync",
|
||||
"System": "System",
|
||||
"TvShows": "TV Shows",
|
||||
"User": "User",
|
||||
"UserCreatedWithName": "User {0} has been created",
|
||||
"UserDeletedWithName": "User {0} has been deleted",
|
||||
"UserDownloadingItemWithValues": "{0} is downloading {1}",
|
||||
"UserLockedOutWithName": "User {0} has been locked out",
|
||||
"UserOfflineFromDevice": "{0} has disconnected from {1}",
|
||||
"UserOnlineFromDevice": "{0} is online from {1}",
|
||||
"UserPasswordChangedWithName": "Password has been changed for user {0}",
|
||||
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
||||
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
|
||||
"SubtitlesDownloadedForItem": "Les sous-titres de {0} ont été téléchargés",
|
||||
"Sync": "Synchroniser",
|
||||
"System": "Système",
|
||||
"TvShows": "Séries Télé",
|
||||
"User": "Utilisateur",
|
||||
"UserCreatedWithName": "L'utilisateur {0} a été créé",
|
||||
"UserDeletedWithName": "L'utilisateur {0} a été supprimé",
|
||||
"UserDownloadingItemWithValues": "{0} est en train de télécharger {1}",
|
||||
"UserLockedOutWithName": "L'utilisateur {0} a été verrouillé",
|
||||
"UserOfflineFromDevice": "{0} s'est déconnecté depuis {1}",
|
||||
"UserOnlineFromDevice": "{0} s'est connecté depuis {1}",
|
||||
"UserPasswordChangedWithName": "Le mot de passe pour l'utilisateur {0} a été modifié",
|
||||
"UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
|
||||
"ValueSpecialEpisodeName": "Spécial - {0}",
|
||||
"VersionNumber": "Version {0}"
|
||||
}
|
||||
|
|
|
|
|
@ -0,0 +1,39 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager;
|
||||
|
||||
namespace Emby.Server.Implementations.Middleware
|
||||
{
|
||||
public class WebSocketMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<WebSocketMiddleware> _logger;
|
||||
private readonly WebSocketManager _webSocketManager;
|
||||
|
||||
public WebSocketMiddleware(RequestDelegate next, ILogger<WebSocketMiddleware> logger, WebSocketManager webSocketManager)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
_webSocketManager = webSocketManager;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
_logger.LogInformation("Handling request: " + httpContext.Request.Path);
|
||||
|
||||
if (httpContext.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
|
||||
if (webSocketContext != null)
|
||||
{
|
||||
await _webSocketManager.OnWebSocketConnected(webSocketContext);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _next.Invoke(httpContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using MediaBrowser.Model.Reflection;
|
||||
|
||||
namespace Emby.Server.Implementations.Reflection
|
||||
{
|
||||
public class AssemblyInfo : IAssemblyInfo
|
||||
{
|
||||
public Stream GetManifestResourceStream(Type type, string resource)
|
||||
{
|
||||
return type.Assembly.GetManifestResourceStream(resource);
|
||||
}
|
||||
|
||||
public string[] GetManifestResourceNames(Type type)
|
||||
{
|
||||
return type.Assembly.GetManifestResourceNames();
|
||||
}
|
||||
|
||||
public Assembly[] GetCurrentAssemblies()
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetAssemblies();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using System.IO;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Jellyfin.Server.SocketSharp
|
||||
namespace Emby.Server.Implementations.SocketSharp
|
||||
{
|
||||
public class HttpFile : IHttpFile
|
||||
{
|
@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using Emby.Server.Implementations.Net;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Emby.Server.Implementations.SocketSharp
|
||||
{
|
||||
public class WebSocketSharpListener : IHttpListener
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
||||
private CancellationToken _disposeCancellationToken;
|
||||
|
||||
public WebSocketSharpListener(
|
||||
ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
_disposeCancellationToken = _disposeCancellationTokenSource.Token;
|
||||
}
|
||||
|
||||
public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
|
||||
public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
|
||||
|
||||
public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
|
||||
|
||||
private static void LogRequest(ILogger logger, HttpRequest request)
|
||||
{
|
||||
var url = request.GetDisplayUrl();
|
||||
|
||||
logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString());
|
||||
}
|
||||
|
||||
public async Task ProcessWebSocketRequest(HttpContext ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogRequest(_logger, ctx.Request);
|
||||
var endpoint = ctx.Connection.RemoteIpAddress.ToString();
|
||||
var url = ctx.Request.GetDisplayUrl();
|
||||
|
||||
var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
|
||||
var socket = new SharpWebSocket(webSocketContext, _logger);
|
||||
|
||||
WebSocketConnected(new WebSocketConnectEventArgs
|
||||
{
|
||||
Url = url,
|
||||
QueryString = ctx.Request.Query,
|
||||
WebSocket = socket,
|
||||
Endpoint = endpoint
|
||||
});
|
||||
|
||||
WebSocketReceiveResult result;
|
||||
var message = new List<byte>();
|
||||
|
||||
do
|
||||
{
|
||||
var buffer = WebSocket.CreateServerBuffer(4096);
|
||||
result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken);
|
||||
message.AddRange(buffer.Array.Take(result.Count));
|
||||
|
||||
if (result.EndOfMessage)
|
||||
{
|
||||
socket.OnReceiveBytes(message.ToArray());
|
||||
message.Clear();
|
||||
}
|
||||
} while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close);
|
||||
|
||||
|
||||
if (webSocketContext.State == WebSocketState.Open)
|
||||
{
|
||||
await webSocketContext.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
|
||||
result.CloseStatusDescription, _disposeCancellationToken);
|
||||
}
|
||||
|
||||
socket.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "AcceptWebSocketAsync error");
|
||||
if (!ctx.Response.HasStarted)
|
||||
{
|
||||
ctx.Response.StatusCode = 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task Stop()
|
||||
{
|
||||
_disposeCancellationTokenSource.Cancel();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the unmanaged resources and disposes of the managed resources used.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the unmanaged resources and disposes of the managed resources used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">Whether or not the managed resources should be disposed</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
Stop().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using IRequest = MediaBrowser.Model.Services.IRequest;
|
||||
|
||||
namespace Emby.Server.Implementations.SocketSharp
|
||||
{
|
||||
public class WebSocketSharpResponse : IResponse
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public WebSocketSharpResponse(ILogger logger, HttpResponse response)
|
||||
{
|
||||
_logger = logger;
|
||||
OriginalResponse = response;
|
||||
}
|
||||
|
||||
public HttpResponse OriginalResponse { get; }
|
||||
|
||||
public int StatusCode
|
||||
{
|
||||
get => OriginalResponse.StatusCode;
|
||||
set => OriginalResponse.StatusCode = value;
|
||||
}
|
||||
|
||||
public string StatusDescription { get; set; }
|
||||
|
||||
public string ContentType
|
||||
{
|
||||
get => OriginalResponse.ContentType;
|
||||
set => OriginalResponse.ContentType = value;
|
||||
}
|
||||
|
||||
public void AddHeader(string name, string value)
|
||||
{
|
||||
if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ContentType = value;
|
||||
return;
|
||||
}
|
||||
|
||||
OriginalResponse.Headers.Add(name, value);
|
||||
}
|
||||
|
||||
public void Redirect(string url)
|
||||
{
|
||||
OriginalResponse.Redirect(url);
|
||||
}
|
||||
|
||||
public Stream OutputStream => OriginalResponse.Body;
|
||||
|
||||
public bool SendChunked { get; set; }
|
||||
|
||||
const int StreamCopyToBufferSize = 81920;
|
||||
public async Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken)
|
||||
{
|
||||
var allowAsync = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
|
||||
//if (count <= 0)
|
||||
//{
|
||||
// allowAsync = true;
|
||||
//}
|
||||
|
||||
var fileOpenOptions = FileOpenOptions.SequentialScan;
|
||||
|
||||
if (allowAsync)
|
||||
{
|
||||
fileOpenOptions |= FileOpenOptions.Asynchronous;
|
||||
}
|
||||
|
||||
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
|
||||
|
||||
using (var fs = fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions))
|
||||
{
|
||||
if (offset > 0)
|
||||
{
|
||||
fs.Position = offset;
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
await streamHelper.CopyToAsync(fs, OutputStream, count, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await fs.CopyToAsync(OutputStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.WebSockets
|
||||
{
|
||||
public interface IWebSocketHandler
|
||||
{
|
||||
Task ProcessMessage(WebSocketMessage<object> message, TaskCompletionSource<bool> taskCompletionSource);
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using UtfUnknown;
|
||||
|
||||
namespace Emby.Server.Implementations.WebSockets
|
||||
{
|
||||
public class WebSocketManager
|
||||
{
|
||||
private readonly IWebSocketHandler[] _webSocketHandlers;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly ILogger<WebSocketManager> _logger;
|
||||
private const int BufferSize = 4096;
|
||||
|
||||
public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger<WebSocketManager> logger)
|
||||
{
|
||||
_webSocketHandlers = webSocketHandlers;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task OnWebSocketConnected(WebSocket webSocket)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource<bool>();
|
||||
var cancellationToken = new CancellationTokenSource().Token;
|
||||
WebSocketReceiveResult result;
|
||||
var message = new List<byte>();
|
||||
|
||||
// Keep listening for incoming messages, otherwise the socket closes automatically
|
||||
do
|
||||
{
|
||||
var buffer = WebSocket.CreateServerBuffer(BufferSize);
|
||||
result = await webSocket.ReceiveAsync(buffer, cancellationToken);
|
||||
message.AddRange(buffer.Array.Take(result.Count));
|
||||
|
||||
if (result.EndOfMessage)
|
||||
{
|
||||
await ProcessMessage(message.ToArray(), taskCompletionSource);
|
||||
message.Clear();
|
||||
}
|
||||
} while (!taskCompletionSource.Task.IsCompleted &&
|
||||
webSocket.State == WebSocketState.Open &&
|
||||
result.MessageType != WebSocketMessageType.Close);
|
||||
|
||||
if (webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
await webSocket.CloseAsync(result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
|
||||
result.CloseStatusDescription, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource<bool> taskCompletionSource)
|
||||
{
|
||||
var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName;
|
||||
var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)
|
||||
? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length)
|
||||
: Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length);
|
||||
|
||||
// All messages are expected to be valid JSON objects
|
||||
if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Received web socket message that is not a json structure: {Message}", message);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var info = _jsonSerializer.DeserializeFromString<WebSocketMessage<object>>(message);
|
||||
|
||||
_logger.LogDebug("Websocket message received: {0}", info.MessageType);
|
||||
|
||||
var tasks = _webSocketHandlers.Select(handler => Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}",
|
||||
handler.GetType().Name, info.MessageType ?? string.Empty);
|
||||
}
|
||||
}));
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing web socket message");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse;
|
||||
using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
|
||||
using IRequest = MediaBrowser.Model.Services.IRequest;
|
||||
|
||||
namespace Jellyfin.Server.SocketSharp
|
||||
{
|
||||
public class WebSocketSharpResponse : IHttpResponse
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly HttpListenerResponse _response;
|
||||
|
||||
public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request)
|
||||
{
|
||||
_logger = logger;
|
||||
this._response = response;
|
||||
Items = new Dictionary<string, object>();
|
||||
Request = request;
|
||||
}
|
||||
|
||||
public IRequest Request { get; private set; }
|
||||
|
||||
public Dictionary<string, object> Items { get; private set; }
|
||||
|
||||
public object OriginalResponse => _response;
|
||||
|
||||
public int StatusCode
|
||||
{
|
||||
get => this._response.StatusCode;
|
||||
set => this._response.StatusCode = value;
|
||||
}
|
||||
|
||||
public string StatusDescription
|
||||
{
|
||||
get => this._response.StatusDescription;
|
||||
set => this._response.StatusDescription = value;
|
||||
}
|
||||
|
||||
public string ContentType
|
||||
{
|
||||
get => _response.ContentType;
|
||||
set => _response.ContentType = value;
|
||||
}
|
||||
|
||||
public QueryParamCollection Headers => _response.Headers;
|
||||
|
||||
private static string AsHeaderValue(Cookie cookie)
|
||||
{
|
||||
DateTime defaultExpires = DateTime.MinValue;
|
||||
|
||||
var path = cookie.Expires == defaultExpires
|
||||
? "/"
|
||||
: cookie.Path ?? "/";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.Append($"{cookie.Name}={cookie.Value};path={path}");
|
||||
|
||||
if (cookie.Expires != defaultExpires)
|
||||
{
|
||||
sb.Append($";expires={cookie.Expires:R}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(cookie.Domain))
|
||||
{
|
||||
sb.Append($";domain={cookie.Domain}");
|
||||
}
|
||||
|
||||
if (cookie.Secure)
|
||||
{
|
||||
sb.Append(";Secure");
|
||||
}
|
||||
|
||||
if (cookie.HttpOnly)
|
||||
{
|
||||
sb.Append(";HttpOnly");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void AddHeader(string name, string value)
|
||||
{
|
||||
if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ContentType = value;
|
||||
return;
|
||||
}
|
||||
|
||||
_response.AddHeader(name, value);
|
||||
}
|
||||
|
||||
public string GetHeader(string name)
|
||||
{
|
||||
return _response.Headers[name];
|
||||
}
|
||||
|
||||
public void Redirect(string url)
|
||||
{
|
||||
_response.Redirect(url);
|
||||
}
|
||||
|
||||
public Stream OutputStream => _response.OutputStream;
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (!this.IsClosed)
|
||||
{
|
||||
this.IsClosed = true;
|
||||
|
||||
try
|
||||
{
|
||||
var response = this._response;
|
||||
|
||||
var outputStream = response.OutputStream;
|
||||
|
||||
// This is needed with compression
|
||||
outputStream.Flush();
|
||||
outputStream.Dispose();
|
||||
|
||||
response.Close();
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in HttpListenerResponseWrapper");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsClosed
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public void SetContentLength(long contentLength)
|
||||
{
|
||||
// you can happily set the Content-Length header in Asp.Net
|
||||
// but HttpListener will complain if you do - you have to set ContentLength64 on the response.
|
||||
// workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header
|
||||
_response.ContentLength64 = contentLength;
|
||||
}
|
||||
|
||||
public void SetCookie(Cookie cookie)
|
||||
{
|
||||
var cookieStr = AsHeaderValue(cookie);
|
||||
_response.Headers.Add("Set-Cookie", cookieStr);
|
||||
}
|
||||
|
||||
public bool SendChunked
|
||||
{
|
||||
get => _response.SendChunked;
|
||||
set => _response.SendChunked = value;
|
||||
}
|
||||
|
||||
public bool KeepAlive { get; set; }
|
||||
|
||||
public void ClearCookies()
|
||||
{
|
||||
}
|
||||
|
||||
public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
|
||||
{
|
||||
return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue