diff options
Diffstat (limited to 'Emby.Server.Implementations')
10 files changed, 187 insertions, 76 deletions
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 179683055..b34d0f21e 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -112,7 +112,8 @@ namespace Emby.Server.Implementations.Collections return Path.Combine(_appPaths.DataPath, "collections"); } - private Task<Folder?> GetCollectionsFolder(bool createIfNeeded) + /// <inheritdoc /> + public Task<Folder?> GetCollectionsFolder(bool createIfNeeded) { return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded); } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ca8f605a0..73ec856fc 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2452,7 +2452,9 @@ namespace Emby.Server.Implementations.Data if (query.SearchTerm.Length > 1) { builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)"); - builder.Append("+ ((Tags not null and Tags like @SearchTermContains) * 5)"); + builder.Append("+ (SELECT COUNT(1) * 1 from ItemValues where ItemId=Guid and CleanValue like @SearchTermContains)"); + builder.Append("+ (SELECT COUNT(1) * 2 from ItemValues where ItemId=Guid and CleanValue like @SearchTermStartsWith)"); + builder.Append("+ (SELECT COUNT(1) * 10 from ItemValues where ItemId=Guid and CleanValue like @SearchTermEquals)"); } builder.Append(") as SearchScore"); @@ -2483,6 +2485,11 @@ namespace Emby.Server.Implementations.Data { statement.TryBind("@SearchTermContains", "%" + searchTerm + "%"); } + + if (commandText.Contains("@SearchTermEquals", StringComparison.OrdinalIgnoreCase)) + { + statement.TryBind("@SearchTermEquals", searchTerm); + } } private void BindSimilarParams(InternalItemsQuery query, IStatement statement) diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index b1a99853a..fd7653a32 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -9,7 +9,8 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Extensions.Json; using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Net; +using MediaBrowser.Controller.Net.WebSocketMessages; +using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -85,14 +86,15 @@ namespace Emby.Server.Implementations.HttpServer /// <value>The state.</value> public WebSocketState State => _socket.State; - /// <summary> - /// Sends a message asynchronously. - /// </summary> - /// <typeparam name="T">The type of the message.</typeparam> - /// <param name="message">The message.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken) + { + var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); + return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); + } + + /// <inheritdoc /> + public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken) { var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); @@ -171,7 +173,7 @@ namespace Emby.Server.Implementations.HttpServer return; } - WebSocketMessage<object>? stub; + InboundWebSocketMessage<object>? stub; long bytesConsumed; try { @@ -212,10 +214,10 @@ namespace Emby.Server.Implementations.HttpServer } } - internal WebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed) + internal InboundWebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed) { var jsonReader = new Utf8JsonReader(bytes); - var ret = JsonSerializer.Deserialize<WebSocketMessage<object>>(ref jsonReader, _jsonOptions); + var ret = JsonSerializer.Deserialize<InboundWebSocketMessage<object>>(ref jsonReader, _jsonOptions); bytesConsumed = jsonReader.BytesConsumed; return ret; } @@ -224,11 +226,7 @@ namespace Emby.Server.Implementations.HttpServer { LastKeepAliveDate = DateTime.UtcNow; return SendAsync( - new WebSocketMessage<string> - { - MessageId = Guid.NewGuid(), - MessageType = SessionMessageType.KeepAlive - }, + new OutboundKeepAliveMessage(), CancellationToken.None); } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 1fffdfbfa..0ba4a488b 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -20,6 +20,14 @@ namespace Emby.Server.Implementations.IO private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); private readonly string _tempPath; private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows(); + private static readonly char[] _invalidPathCharacters = + { + '\"', '<', '>', '|', '\0', + (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, + (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, + (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, + (char)31, ':', '*', '?', '\\', '/' + }; /// <summary> /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class. @@ -275,8 +283,7 @@ namespace Emby.Server.Implementations.IO /// <exception cref="ArgumentNullException">The filename is null.</exception> public string GetValidFilename(string filename) { - var invalid = Path.GetInvalidFileNameChars(); - var first = filename.IndexOfAny(invalid); + var first = filename.IndexOfAny(_invalidPathCharacters); if (first == -1) { // Fast path for clean strings @@ -285,7 +292,7 @@ namespace Emby.Server.Implementations.IO return string.Create( filename.Length, - (filename, invalid, first), + (filename, _invalidPathCharacters, first), (chars, state) => { state.filename.AsSpan().CopyTo(chars); @@ -293,7 +300,7 @@ namespace Emby.Server.Implementations.IO chars[state.first++] = ' '; var len = chars.Length; - foreach (var c in state.invalid) + foreach (var c in state._invalidPathCharacters) { for (int i = state.first; i < len; i++) { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index ea45bf0ba..8bb2d3c02 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2069,7 +2069,9 @@ namespace Emby.Server.Implementations.Library .Find(folder => folder is CollectionFolder) as CollectionFolder; } - return collectionFolder is null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); + return collectionFolder is null + ? new LibraryOptions() + : collectionFolder.GetLibraryOptions(); } public string GetContentType(BaseItem item) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index bcb42e162..acf3964c8 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -30,12 +30,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { - private static readonly string[] _disallowedSharedStreamExtensions = + private static readonly string[] _disallowedMimeTypes = { - ".mkv", - ".mp4", - ".m3u8", - ".mpd" + "video/x-matroska", + "video/mp4", + "application/vnd.apple.mpegurl", + "application/mpegurl", + "application/x-mpegurl", + "video/vnd.mpeg.dash.mpd" }; private readonly IHttpClientFactory _httpClientFactory; @@ -118,9 +120,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) { - var extension = Path.GetExtension(mediaSource.Path) ?? string.Empty; + using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(message, cancellationToken) + .ConfigureAwait(false); - if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + response.EnsureSuccessStatusCode(); + + if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.ToString(), StringComparison.OrdinalIgnoreCase)) { return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index e84e1e074..51f46f4da 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts _httpClientFactory = httpClientFactory; _appHost = appHost; OriginalStreamId = originalStreamId; - EnableStreamSharing = true; } public override async Task Open(CancellationToken openCancellationToken) @@ -59,39 +58,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) .ConfigureAwait(false); - var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty; - if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("dash", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase) - || contentType.Contains("text/", StringComparison.OrdinalIgnoreCase)) - { - // Close the stream without any sharing features - response.Dispose(); - return; - } - - SetTempFilePath("ts"); - var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); _ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token); - // OpenedMediaSource.Protocol = MediaProtocol.File; - // OpenedMediaSource.Path = tempFile; - // OpenedMediaSource.ReadAtNativeFramerate = true; - MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Protocol = MediaProtocol.Http; - // OpenedMediaSource.Path = TempFilePath; - // OpenedMediaSource.Protocol = MediaProtocol.File; - - // OpenedMediaSource.Path = _tempFilePath; - // OpenedMediaSource.Protocol = MediaProtocol.File; - // OpenedMediaSource.SupportsDirectPlay = false; - // OpenedMediaSource.SupportsDirectStream = true; - // OpenedMediaSource.SupportsTranscoding = true; var res = await taskCompletionSource.Task.ConfigureAwait(false); if (!res) { @@ -108,15 +81,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts try { Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath); - using var message = response; - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await StreamHelper.CopyToAsync( - stream, - fileStream, - IODefaults.CopyToBufferSize, - () => Resolve(openTaskCompletionSource), - cancellationToken).ConfigureAwait(false); + using (response) + { + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + await StreamHelper.CopyToAsync( + stream, + fileStream, + IODefaults.CopyToBufferSize, + () => Resolve(openTaskCompletionSource), + cancellationToken).ConfigureAwait(false); + } } catch (OperationCanceledException ex) { diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs new file mode 100644 index 000000000..f78fc6f97 --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// <summary> +/// Deletes Path references from collections that no longer exists. +/// </summary> +public class CleanupCollectionPathsTask : IScheduledTask +{ + private readonly ILocalizationManager _localization; + private readonly ICollectionManager _collectionManager; + private readonly ILogger<CleanupCollectionPathsTask> _logger; + private readonly IProviderManager _providerManager; + private readonly IFileSystem _fileSystem; + + /// <summary> + /// Initializes a new instance of the <see cref="CleanupCollectionPathsTask"/> class. + /// </summary> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param> + /// <param name="logger">The logger.</param> + /// <param name="providerManager">The provider manager.</param> + /// <param name="fileSystem">The filesystem.</param> + public CleanupCollectionPathsTask( + ILocalizationManager localization, + ICollectionManager collectionManager, + ILogger<CleanupCollectionPathsTask> logger, + IProviderManager providerManager, + IFileSystem fileSystem) + { + _localization = localization; + _collectionManager = collectionManager; + _logger = logger; + _providerManager = providerManager; + _fileSystem = fileSystem; + } + + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanCollections"); + + /// <inheritdoc /> + public string Key => "CleanCollections"; + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskCleanCollectionsDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false); + if (collectionsFolder is null) + { + _logger.LogDebug("There is no collection folder to be found"); + return; + } + + var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray(); + _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length); + + var itemsToRemove = new List<LinkedChild>(); + for (var index = 0; index < collections.Length; index++) + { + var collection = collections[index]; + _logger.LogDebug("Check Boxset {CollectionName}", collection.Name); + + foreach (var collectionLinkedChild in collection.LinkedChildren) + { + if (!File.Exists(collectionLinkedChild.Path)) + { + _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}", collection.Name, collectionLinkedChild.Path); + itemsToRemove.Add(collectionLinkedChild); + } + } + + if (itemsToRemove.Count != 0) + { + _logger.LogDebug("Update Boxset {CollectionName}", collection.Name); + collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray(); + await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken) + .ConfigureAwait(false); + + _providerManager.QueueRefresh( + collection.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = true + }, + RefreshPriority.High); + + itemsToRemove.Clear(); + } + + progress.Report(100D / collections.Length * (index + 1)); + } + } + + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } }; + } +} diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 4e427b1a4..b3c93a904 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -6,9 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -308,11 +307,7 @@ namespace Emby.Server.Implementations.Session private Task SendForceKeepAlive(IWebSocketConnection webSocket) { return webSocket.SendAsync( - new WebSocketMessage<int> - { - MessageType = SessionMessageType.ForceKeepAlive, - Data = WebSocketLostTimeout - }, + new ForceKeepAliveMessage(WebSocketLostTimeout), CancellationToken.None); } diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index cdc736950..cf8e0fb00 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -7,8 +7,8 @@ using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Net.WebSocketMessages; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Session } return socket.SendAsync( - new WebSocketMessage<T> + new OutboundWebSocketMessage<T> { Data = data, MessageType = name, |
