Selaa lähdekoodia

Merge branch 'release-10.8.z' into fix-hevc-disable-option

Joshua M. Boniface 3 vuotta sitten
vanhempi
sitoutus
85cfea4c50
36 muutettua tiedostoa jossa 344 lisäystä ja 122 poistoa
  1. 1 1
      Emby.Dlna/Service/BaseControlHandler.cs
  2. 1 1
      Emby.Naming/Common/NamingOptions.cs
  3. 3 4
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  4. 5 1
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  5. 1 1
      Emby.Server.Implementations/Library/SearchEngine.cs
  6. 9 2
      Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
  7. 12 2
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  8. 1 1
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  9. 1 1
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  10. 1 1
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  11. 0 1
      MediaBrowser.Controller/Entities/BaseItem.cs
  12. 1 1
      MediaBrowser.Controller/Entities/Genre.cs
  13. 1 1
      MediaBrowser.Controller/Entities/Person.cs
  14. 1 1
      MediaBrowser.Controller/Entities/Studio.cs
  15. 1 1
      MediaBrowser.Controller/Library/NameExtensions.cs
  16. 0 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  17. 79 27
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  18. 7 0
      MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
  19. 13 0
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  20. 14 5
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  21. 1 1
      MediaBrowser.Model/Configuration/LibraryOptions.cs
  22. 24 25
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  23. 1 1
      MediaBrowser.Providers/Manager/MetadataService.cs
  24. 1 1
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  25. 31 8
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  26. 1 1
      MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
  27. 6 0
      debian/changelog
  28. 1 1
      debian/metapackage/jellyfin
  29. 3 1
      fedora/jellyfin.spec
  30. 42 0
      src/Jellyfin.Extensions/StringExtensions.cs
  31. 32 0
      tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs
  32. 28 21
      tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
  33. 4 4
      tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json
  34. 4 4
      tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json
  35. 2 1
      tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs
  36. 11 0
      tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs

+ 1 - 1
Emby.Dlna/Service/BaseControlHandler.cs

@@ -6,8 +6,8 @@ using System.IO;
 using System.Text;
 using System.Threading.Tasks;
 using System.Xml;
-using Diacritics.Extensions;
 using Emby.Dlna.Didl;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Configuration;
 using Microsoft.Extensions.Logging;
 

+ 1 - 1
Emby.Naming/Common/NamingOptions.cs

@@ -314,7 +314,7 @@ namespace Emby.Naming.Common
                 // This isn't a Kodi naming rule, but the expression below causes false positives,
                 // so we make sure this one gets tested first.
                 // "Foo Bar 889"
-                new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
+                new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
                 {
                     IsNamed = true
                 },

+ 3 - 4
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -11,7 +11,6 @@ using System.Linq;
 using System.Text;
 using System.Text.Json;
 using System.Threading;
-using Diacritics.Extensions;
 using Emby.Server.Implementations.Playlists;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
@@ -5763,7 +5762,7 @@ AND Type = @InternalPersonType)");
                 {
                     var itemIdBlob = id.ToByteArray();
 
-                    // First delete chapters
+                    // Delete existing mediastreams
                     db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob);
 
                     InsertMediaStreams(itemIdBlob, streams, db);
@@ -5867,10 +5866,10 @@ AND Type = @InternalPersonType)");
         }
 
         /// <summary>
-        /// Gets the chapter.
+        /// Gets the media stream.
         /// </summary>
         /// <param name="reader">The reader.</param>
-        /// <returns>ChapterInfo.</returns>
+        /// <returns>MediaStream.</returns>
         private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader)
         {
             var item = new MediaStream

+ 5 - 1
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -151,7 +151,11 @@ namespace Emby.Server.Implementations.Library
         {
             var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
 
-            if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video))
+            // If file is strm or main media stream is missing, force a metadata refresh with remote probing
+            if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
+                && (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase)
+                    || (item.MediaType == MediaType.Video && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Video))
+                    || (item.MediaType == MediaType.Audio && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio))))
             {
                 await item.RefreshMetadata(
                     new MetadataRefreshOptions(_directoryService)

+ 1 - 1
Emby.Server.Implementations/Library/SearchEngine.cs

@@ -5,9 +5,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using Diacritics.Extensions;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;

+ 9 - 2
Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs

@@ -3,6 +3,7 @@
 using System;
 using System.Globalization;
 using MediaBrowser.Controller.LiveTv;
+using System.Text;
 
 namespace Emby.Server.Implementations.LiveTv.EmbyTV
 {
@@ -48,12 +49,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 if (!string.IsNullOrWhiteSpace(info.EpisodeTitle))
                 {
+                    var tmpName = name;
                     if (addHyphen)
                     {
-                        name += " -";
+                        tmpName += " -";
                     }
 
-                    name += " " + info.EpisodeTitle;
+                    tmpName += " " + info.EpisodeTitle;
+                    //  Since the filename will be used with file ext. (.mp4, .ts, etc)
+                    if (Encoding.UTF8.GetByteCount(tmpName) < 250)
+                    {
+                        name = tmpName;
+                    }
                 }
             }
             else if (info.IsMovie && info.ProductionYear != null)

+ 12 - 2
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -1773,13 +1773,23 @@ namespace Jellyfin.Api.Controllers
 
             var args = "-codec:v:0 " + codec;
 
-            // Prefer hvc1 to hev1.
             if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
                 || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
                 || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
                 || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
             {
-                args += " -tag:v:0 hvc1";
+                if (EncodingHelper.IsCopyCodec(codec)
+                    && (string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
+                        || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
+                {
+                    // Prefer dvh1 to dvhe
+                    args += " -tag:v:0 dvh1";
+                }
+                else
+                {
+                    // Prefer hvc1 to hev1
+                    args += " -tag:v:0 hvc1";
+                }
             }
 
             // if  (state.EnableMpegtsM2TsMode)

+ 1 - 1
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using BlurHashSharp.SkiaSharp;
-using Diacritics.Extensions;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Drawing;

+ 1 - 1
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -8,9 +8,9 @@ using System.Linq;
 using System.Text.Json.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
-using Diacritics.Extensions;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using Microsoft.Extensions.Logging;

+ 1 - 1
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -5,8 +5,8 @@
 using System;
 using System.Collections.Generic;
 using System.Text.Json.Serialization;
-using Diacritics.Extensions;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Controller.Entities.Audio

+ 0 - 1
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -11,7 +11,6 @@ using System.Text;
 using System.Text.Json.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
-using Diacritics.Extensions;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;

+ 1 - 1
MediaBrowser.Controller/Entities/Genre.cs

@@ -5,8 +5,8 @@
 using System;
 using System.Collections.Generic;
 using System.Text.Json.Serialization;
-using Diacritics.Extensions;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Controller.Entities

+ 1 - 1
MediaBrowser.Controller/Entities/Person.cs

@@ -5,7 +5,7 @@
 using System;
 using System.Collections.Generic;
 using System.Text.Json.Serialization;
-using Diacritics.Extensions;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Providers;
 using Microsoft.Extensions.Logging;
 

+ 1 - 1
MediaBrowser.Controller/Entities/Studio.cs

@@ -5,7 +5,7 @@
 using System;
 using System.Collections.Generic;
 using System.Text.Json.Serialization;
-using Diacritics.Extensions;
+using Jellyfin.Extensions;
 using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Controller.Entities

+ 1 - 1
MediaBrowser.Controller/Library/NameExtensions.cs

@@ -3,7 +3,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using Diacritics.Extensions;
+using Jellyfin.Extensions;
 
 namespace MediaBrowser.Controller.Library
 {

+ 0 - 1
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -18,7 +18,6 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Diacritics" Version="3.3.10" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />

+ 79 - 27
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -2215,13 +2215,13 @@ namespace MediaBrowser.Controller.MediaEncoding
                 return state.IsInputVideo ? "-sn" : string.Empty;
             }
 
-            // We have media info, but we don't know the stream indexes
+            // We have media info, but we don't know the stream index
             if (state.VideoStream != null && state.VideoStream.Index == -1)
             {
                 return "-sn";
             }
 
-            // We have media info, but we don't know the stream indexes
+            // We have media info, but we don't know the stream index
             if (state.AudioStream != null && state.AudioStream.Index == -1)
             {
                 return state.IsInputVideo ? "-sn" : string.Empty;
@@ -2231,10 +2231,12 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             if (state.VideoStream != null)
             {
+                int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
+
                 args += string.Format(
                     CultureInfo.InvariantCulture,
                     "-map 0:{0}",
-                    state.VideoStream.Index);
+                    videoStreamIndex);
             }
             else
             {
@@ -2244,24 +2246,24 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             if (state.AudioStream != null)
             {
+                int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream);
                 if (state.AudioStream.IsExternal)
                 {
                     bool hasExternalGraphicsSubs = state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream;
                     int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1;
-                    int externalAudioStream = state.MediaSource.MediaStreams.Where(i => i.Path == state.AudioStream.Path).ToList().IndexOf(state.AudioStream);
 
                     args += string.Format(
                         CultureInfo.InvariantCulture,
                         " -map {0}:{1}",
                         externalAudioMapIndex,
-                        externalAudioStream);
+                        audioStreamIndex);
                 }
                 else
                 {
                     args += string.Format(
                         CultureInfo.InvariantCulture,
                         " -map 0:{0}",
-                        state.AudioStream.Index);
+                        audioStreamIndex);
                 }
             }
             else
@@ -2276,14 +2278,21 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
             else if (subtitleMethod == SubtitleDeliveryMethod.Embed)
             {
+                int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
+
                 args += string.Format(
                     CultureInfo.InvariantCulture,
                     " -map 0:{0}",
-                    state.SubtitleStream.Index);
+                    subtitleStreamIndex);
             }
             else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
             {
-                args += " -map 1:0 -sn";
+                int externalSubtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
+
+                args += string.Format(
+                    CultureInfo.InvariantCulture,
+                    " -map 1:{0} -sn",
+                    externalSubtitleStreamIndex);
             }
 
             return args;
@@ -2512,7 +2521,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
                 return string.Format(
                         CultureInfo.InvariantCulture,
-                        "scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/{2})*{2}:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2",
+                        "scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2",
                         maxWidthParam,
                         maxHeightParam,
                         scaleVal);
@@ -2556,7 +2565,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
                 return string.Format(
                         CultureInfo.InvariantCulture,
-                        "scale=trunc(min(max(iw\\,ih*dar)\\,{0})/{1})*{1}:trunc(ow/dar/2)*2",
+                        "scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2",
                         maxWidthParam,
                         scaleVal);
             }
@@ -2568,7 +2577,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
                 return string.Format(
                         CultureInfo.InvariantCulture,
-                        "scale=trunc(oh*a/{1})*{1}:min(max(iw/dar\\,ih)\\,{0})",
+                        "scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})",
                         maxHeightParam,
                         scaleVal);
             }
@@ -2617,7 +2626,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 }
                 else
                 {
-                    filter = "scale={0}:trunc({0}/dar/2)*2";
+                    filter = "scale={0}:trunc({0}/a/2)*2";
                 }
             }
 
@@ -2771,8 +2780,8 @@ namespace MediaBrowser.Controller.MediaEncoding
             }
             else if (hasGraphicalSubs)
             {
-                // [0:s]scale=s=1280x720
-                var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+                // [0:s]scale=expr
+                var subSwScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
                 subFilters.Add(subSwScaleFilter);
                 overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
             }
@@ -2958,7 +2967,9 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 if (hasGraphicalSubs)
                 {
-                    var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+                    var subSwScaleFilter = isSwDecoder
+                        ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
+                        : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
                     subFilters.Add(subSwScaleFilter);
                     overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
                 }
@@ -3156,7 +3167,9 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 if (hasGraphicalSubs)
                 {
-                    var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+                    var subSwScaleFilter = isSwDecoder
+                        ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
+                        : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
                     subFilters.Add(subSwScaleFilter);
                     overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
                 }
@@ -3402,7 +3415,9 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 if (hasGraphicalSubs)
                 {
-                    var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+                    var subSwScaleFilter = isSwDecoder
+                        ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
+                        : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
                     subFilters.Add(subSwScaleFilter);
                     overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
                 }
@@ -3611,7 +3626,9 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 if (hasGraphicalSubs)
                 {
-                    var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+                    var subSwScaleFilter = isSwDecoder
+                        ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
+                        : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
                     subFilters.Add(subSwScaleFilter);
                     overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
                 }
@@ -3858,7 +3875,9 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 if (hasGraphicalSubs)
                 {
-                    var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+                    var subSwScaleFilter = isSwDecoder
+                        ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
+                        : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
                     subFilters.Add(subSwScaleFilter);
                     overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
 
@@ -4033,7 +4052,9 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
                 if (hasGraphicalSubs)
                 {
-                    var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+                    var subSwScaleFilter = isSwDecoder
+                        ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
+                        : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
                     subFilters.Add(subSwScaleFilter);
                     overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
 
@@ -4129,9 +4150,8 @@ namespace MediaBrowser.Controller.MediaEncoding
                         string.Join(',', overlayFilters));
 
                 var mapPrefix = Convert.ToInt32(state.SubtitleStream.IsExternal);
-                var subtitleStreamIndex = state.SubtitleStream.IsExternal
-                    ? 0
-                    : state.SubtitleStream.Index;
+                var subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
+                var videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
 
                 if (hasSubs)
                 {
@@ -4152,7 +4172,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                         filterStr,
                         mapPrefix,
                         subtitleStreamIndex,
-                        state.VideoStream.Index,
+                        videoStreamIndex,
                         mainStr,
                         subStr,
                         overlayStr);
@@ -5362,12 +5382,22 @@ namespace MediaBrowser.Controller.MediaEncoding
                 audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
             }
 
-            // opus will fail on 44100
             if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
             {
-                if (state.OutputAudioSampleRate.HasValue)
+                // opus only supports specific sampling rates
+                var sampleRate = state.OutputAudioSampleRate;
+                if (sampleRate.HasValue)
                 {
-                    audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
+                    var sampleRateValue = sampleRate.Value switch
+                    {
+                        <= 8000 => 8000,
+                        <= 12000 => 12000,
+                        <= 16000 => 16000,
+                        <= 24000 => 24000,
+                        _ => 48000
+                    };
+
+                    audioTranscodeParams.Add("-ar " + sampleRateValue.ToString(CultureInfo.InvariantCulture));
                 }
             }
 
@@ -5389,6 +5419,28 @@ namespace MediaBrowser.Controller.MediaEncoding
                 string.Empty).Trim();
         }
 
+        public static int FindIndex(IReadOnlyList<MediaStream> mediaStreams, MediaStream streamToFind)
+        {
+            var index = 0;
+            var length = mediaStreams.Count;
+
+            for (var i = 0; i < length; i++)
+            {
+                var currentMediaStream = mediaStreams[i];
+                if (currentMediaStream == streamToFind)
+                {
+                    return index;
+                }
+
+                 if (string.Equals(currentMediaStream.Path, streamToFind.Path, StringComparison.Ordinal))
+                {
+                    index++;
+                }
+            }
+
+            return -1;
+        }
+
         public static bool IsCopyCodec(string codec)
         {
             return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase);

+ 7 - 0
MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs

@@ -141,6 +141,13 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <returns>System.String.</returns>
         string GetInputArgument(string inputFile, MediaSourceInfo mediaSource);
 
+        /// <summary>
+        /// Gets the input argument for an external subtitle file.
+        /// </summary>
+        /// <param name="inputFile">The input file.</param>
+        /// <returns>System.String.</returns>
+        string GetExternalSubtitleInputArgument(string inputFile);
+
         /// <summary>
         /// Gets the time parameter.
         /// </summary>

+ 13 - 0
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -411,6 +411,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
             return EncodingUtils.GetInputArgument(prefix, inputFile, mediaSource.Protocol);
         }
 
+        /// <summary>
+        /// Gets the input argument for an external subtitle file.
+        /// </summary>
+        /// <param name="inputFile">The input file.</param>
+        /// <returns>System.String.</returns>
+        /// <exception cref="ArgumentException">Unrecognized InputType.</exception>
+        public string GetExternalSubtitleInputArgument(string inputFile)
+        {
+            const string Prefix = "file";
+
+            return EncodingUtils.GetInputArgument(Prefix, inputFile, MediaProtocol.File);
+        }
+
         /// <summary>
         /// Gets the media info internal.
         /// </summary>

+ 14 - 5
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -195,7 +195,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             MediaStream subtitleStream,
             CancellationToken cancellationToken)
         {
-            if (!subtitleStream.IsExternal)
+            if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
             {
                 string outputFormat;
                 string outputCodec;
@@ -224,7 +224,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 // Extract
                 var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat);
 
-                await ExtractTextSubtitle(mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
+                await ExtractTextSubtitle(mediaSource, subtitleStream, outputCodec, outputPath, cancellationToken)
                         .ConfigureAwait(false);
 
                 return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false);
@@ -494,7 +494,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         /// Extracts the text subtitle.
         /// </summary>
         /// <param name="mediaSource">The mediaSource.</param>
-        /// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
+        /// <param name="subtitleStream">The subtitle stream.</param>
         /// <param name="outputCodec">The output codec.</param>
         /// <param name="outputPath">The output path.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
@@ -502,7 +502,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         /// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
         private async Task ExtractTextSubtitle(
             MediaSourceInfo mediaSource,
-            int subtitleStreamIndex,
+            MediaStream subtitleStream,
             string outputCodec,
             string outputPath,
             CancellationToken cancellationToken)
@@ -511,12 +511,21 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 
             await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
 
+            var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
+
             try
             {
                 if (!File.Exists(outputPath))
                 {
+                    var args = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
+
+                    if (subtitleStream.IsExternal)
+                    {
+                        args = _mediaEncoder.GetExternalSubtitleInputArgument(subtitleStream.Path);
+                    }
+
                     await ExtractTextSubtitleInternal(
-                        _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource),
+                        args,
                         subtitleStreamIndex,
                         outputCodec,
                         outputPath,

+ 1 - 1
MediaBrowser.Model/Configuration/LibraryOptions.cs

@@ -17,7 +17,7 @@ namespace MediaBrowser.Model.Configuration
             RequirePerfectSubtitleMatch = true;
             AllowEmbeddedSubtitles = EmbeddedSubtitleOptions.AllowAll;
 
-            AutomaticallyAddToCollection = true;
+            AutomaticallyAddToCollection = false;
             EnablePhotos = true;
             SaveSubtitlesWithMedia = true;
             EnableRealtimeMonitor = true;

+ 24 - 25
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -385,7 +385,7 @@ namespace MediaBrowser.Model.Dlna
             // If device requirements are satisfied then allow both direct stream and direct play
             if (item.SupportsDirectPlay)
             {
-                if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay))
+                if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay))
                 {
                     if (options.EnableDirectPlay)
                     {
@@ -401,7 +401,7 @@ namespace MediaBrowser.Model.Dlna
             // While options takes the network and other factors into account. Only applies to direct stream
             if (item.SupportsDirectStream)
             {
-                if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
+                if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
                 {
                     if (options.EnableDirectStream)
                     {
@@ -604,11 +604,11 @@ namespace MediaBrowser.Model.Dlna
 
             var videoStream = item.VideoStream;
 
-            var directPlayEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay);
-            var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream);
-            bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult == 0);
-            bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directPlayEligibilityResult == 0);
-            var transcodeReasons = directPlayEligibilityResult | directStreamEligibilityResult;
+            var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay);
+            var directStreamBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream);
+            bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayBitrateEligibility == 0);
+            bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamBitrateEligibility == 0);
+            var transcodeReasons = directPlayBitrateEligibility | directStreamBitrateEligibility;
 
             _logger.LogDebug(
                 "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
@@ -625,7 +625,7 @@ namespace MediaBrowser.Model.Dlna
                 var directPlay = directPlayInfo.PlayMethod;
                 transcodeReasons |= directPlayInfo.TranscodeReasons;
 
-                if (directPlay != null)
+                if (directPlay.HasValue)
                 {
                     directPlayProfile = directPlayInfo.Profile;
                     playlistItem.PlayMethod = directPlay.Value;
@@ -676,7 +676,7 @@ namespace MediaBrowser.Model.Dlna
 
             playlistItem.TranscodeReasons = transcodeReasons;
 
-            if (playlistItem.PlayMethod != PlayMethod.DirectStream || !options.EnableDirectStream)
+            if (playlistItem.PlayMethod != PlayMethod.DirectStream && playlistItem.PlayMethod != PlayMethod.DirectPlay)
             {
                 // Can't direct play, find the transcoding profile
                 // If we do this for direct-stream we will overwrite the info
@@ -687,6 +687,8 @@ namespace MediaBrowser.Model.Dlna
 
                     BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, transcodingProfile.Container, transcodingProfile.VideoCodec, transcodingProfile.AudioCodec);
 
+                    playlistItem.PlayMethod = PlayMethod.Transcode;
+
                     if (subtitleStream != null)
                     {
                         var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol);
@@ -696,14 +698,9 @@ namespace MediaBrowser.Model.Dlna
                         playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format };
                     }
 
-                    if (playlistItem.PlayMethod != PlayMethod.DirectPlay)
+                    if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0)
                     {
-                        playlistItem.PlayMethod = PlayMethod.Transcode;
-
-                        if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0)
-                        {
-                            ApplyTranscodingConditions(playlistItem, transcodingProfile.Conditions, null, true, true);
-                        }
+                        ApplyTranscodingConditions(playlistItem, transcodingProfile.Conditions, null, true, true);
                     }
                 }
             }
@@ -771,6 +768,7 @@ namespace MediaBrowser.Model.Dlna
 
         private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
         {
+            // Prefer matching video codecs
             var videoCodecs = ContainerProfile.SplitValue(videoCodec);
             var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null;
             if (directVideoCodec != null)
@@ -782,7 +780,7 @@ namespace MediaBrowser.Model.Dlna
 
             playlistItem.VideoCodecs = videoCodecs;
 
-            // copy video codec options as a starting point, this applies to transcode and direct-stream
+            // Copy video codec options as a starting point, this applies to transcode and direct-stream
             playlistItem.MaxFramerate = videoStream?.AverageFrameRate;
             var qualifier = videoStream?.Codec;
             if (videoStream?.Level != null)
@@ -805,7 +803,7 @@ namespace MediaBrowser.Model.Dlna
                 playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString());
             }
 
-            // prefer matching audio codecs, could do better here
+            // Prefer matching audio codecs, could do better here
             var audioCodecs = ContainerProfile.SplitValue(audioCodec);
             var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec));
             playlistItem.AudioCodecs = audioCodecs;
@@ -815,7 +813,7 @@ namespace MediaBrowser.Model.Dlna
                 playlistItem.AudioStreamIndex = audioStream.Index;
                 playlistItem.AudioCodecs = new[] { audioStream.Codec };
 
-                // copy matching audio codec options
+                // Copy matching audio codec options
                 playlistItem.AudioSampleRate = audioStream.SampleRate;
                 playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString());
 
@@ -1076,7 +1074,7 @@ namespace MediaBrowser.Model.Dlna
             DeviceProfile profile = options.Profile;
             string container = mediaSource.Container;
 
-            // video
+            // Video
             int? width = videoStream?.Width;
             int? height = videoStream?.Height;
             int? bitDepth = videoStream?.BitDepth;
@@ -1088,7 +1086,7 @@ namespace MediaBrowser.Model.Dlna
             bool? isInterlaced = videoStream?.IsInterlaced;
             string videoCodecTag = videoStream?.CodecTag;
             bool? isAvc = videoStream?.IsAVC;
-            // audio
+            // Audio
             var defaultLanguage = audioStream?.Language ?? string.Empty;
             var defaultMarked = audioStream?.IsDefault ?? false;
 
@@ -1217,6 +1215,7 @@ namespace MediaBrowser.Model.Dlna
                     return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked);
                 })
                 .OrderByDescending(analysis => analysis.Result.PlayMethod)
+                .ThenByDescending(analysis => analysis.Rank)
                 .ThenBy(analysis => analysis.Order)
                 .ToArray()
                 .ToLookup(analysis => analysis.Result.PlayMethod != null);
@@ -1229,7 +1228,7 @@ namespace MediaBrowser.Model.Dlna
                 return profileMatch;
             }
 
-            var failureReasons = analyzedProfiles[false].OrderBy(a => a.Result.TranscodeReason).ThenBy(analysis => analysis.Order).FirstOrDefault().Result.TranscodeReason;
+            var failureReasons = analyzedProfiles[false].Select(analysis => analysis.Result).FirstOrDefault().TranscodeReason;
             if (failureReasons == 0)
             {
                 failureReasons = TranscodeReason.DirectPlayError;
@@ -1275,13 +1274,13 @@ namespace MediaBrowser.Model.Dlna
                 mediaSource.Path ?? "Unknown path");
         }
 
-        private TranscodeReason IsEligibleForDirectPlay(
+        private TranscodeReason IsBitrateEligibleForDirectPlayback(
             MediaSourceInfo item,
             long maxBitrate,
             VideoOptions options,
             PlayMethod playMethod)
         {
-            bool result = IsItemBitrateEligibleForDirectPlay(item, maxBitrate, playMethod);
+            bool result = IsItemBitrateEligibleForDirectPlayback(item, maxBitrate, playMethod);
             if (!result)
             {
                 return TranscodeReason.ContainerBitrateExceedsLimit;
@@ -1449,7 +1448,7 @@ namespace MediaBrowser.Model.Dlna
             return null;
         }
 
-        private bool IsItemBitrateEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
+        private bool IsItemBitrateEligibleForDirectPlayback(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
         {
             // Don't restrict by bitrate if coming from an external domain
             if (item.IsRemote)

+ 1 - 1
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -8,7 +8,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using Diacritics.Extensions;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;

+ 1 - 1
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -22,7 +22,7 @@
     <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
     <PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
     <PackageReference Include="PlaylistsNET" Version="1.1.3" />
-    <PackageReference Include="TMDbLib" Version="1.9.1" />
+    <PackageReference Include="TMDbLib" Version="1.9.2" />
   </ItemGroup>
 
   <PropertyGroup>

+ 31 - 8
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -173,16 +173,30 @@ namespace MediaBrowser.Providers.MediaInfo
             IReadOnlyList<MediaAttachment> mediaAttachments;
             ChapterInfo[] chapters;
 
+            mediaStreams = new List<MediaStream>();
+
+            // Add external streams before adding the streams from the file to preserve stream IDs on remote videos
+            await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
+
+            await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
+
+            var startIndex = mediaStreams.Count == 0 ? 0 : (mediaStreams.Max(i => i.Index) + 1);
+
             if (mediaInfo != null)
             {
-                mediaStreams = mediaInfo.MediaStreams.ToList();
+                foreach (var mediaStream in mediaInfo.MediaStreams)
+                {
+                    mediaStream.Index = startIndex++;
+                    mediaStreams.Add(mediaStream);
+                }
+
                 mediaAttachments = mediaInfo.MediaAttachments;
 
                 video.TotalBitrate = mediaInfo.Bitrate;
                 // video.FormatName = (mediaInfo.Container ?? string.Empty)
                 //    .Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase);
 
-                // For dvd's this may not always be accurate, so don't set the runtime if the item already has one
+                // For DVDs this may not always be accurate, so don't set the runtime if the item already has one
                 var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks == null || video.RunTimeTicks.Value == 0;
 
                 if (needToSetRuntime)
@@ -213,15 +227,20 @@ namespace MediaBrowser.Providers.MediaInfo
             }
             else
             {
-                mediaStreams = new List<MediaStream>();
+                var currentMediaStreams = video.GetMediaStreams();
+                foreach (var mediaStream in currentMediaStreams)
+                {
+                    if (!mediaStream.IsExternal)
+                    {
+                        mediaStream.Index = startIndex++;
+                        mediaStreams.Add(mediaStream);
+                    }
+                }
+
                 mediaAttachments = Array.Empty<MediaAttachment>();
                 chapters = Array.Empty<ChapterInfo>();
             }
 
-            await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
-
-            await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
-
             var libraryOptions = _libraryManager.GetLibraryOptions(video);
 
             if (mediaInfo != null)
@@ -254,7 +273,11 @@ namespace MediaBrowser.Providers.MediaInfo
             video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle);
 
             _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
-            _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
+
+            if (mediaAttachments.Any())
+            {
+                _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
+            }
 
             if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
                 options.MetadataRefreshMode == MetadataRefreshMode.Default)

+ 1 - 1
MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs

@@ -13,7 +13,7 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
-using Diacritics.Extensions;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;

+ 6 - 0
debian/changelog

@@ -1,3 +1,9 @@
+jellyfin-server (10.8.0~beta3) unstable; urgency=medium
+
+  * New upstream version 10.8.0-beta3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0-beta3
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org>  Sun, 15 May 2022 20:15:43 -0400
+
 jellyfin-server (10.8.0~beta2) unstable; urgency=medium
 
   * New upstream version 10.8.0-beta2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0-beta2

+ 1 - 1
debian/metapackage/jellyfin

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

+ 3 - 1
fedora/jellyfin.spec

@@ -7,7 +7,7 @@
 %endif
 
 Name:           jellyfin
-Version:        10.8.0~beta2
+Version:        10.8.0~beta3
 Release:        1%{?dist}
 Summary:        The Free Software Media System
 License:        GPLv3
@@ -153,6 +153,8 @@ fi
 %systemd_postun_with_restart jellyfin.service
 
 %changelog
+* Sun May 15 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
+- New upstream version 10.8.0-beta3; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0-beta3
 * Sun Apr 17 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
 - New upstream version 10.8.0-beta2; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0-beta2
 * Fri Mar 25 2022 Jellyfin Packaging Team <packaging@jellyfin.org>

+ 42 - 0
src/Jellyfin.Extensions/StringExtensions.cs

@@ -1,4 +1,8 @@
 using System;
+using System.Diagnostics;
+using System.Globalization;
+using System.Text;
+using System.Text.RegularExpressions;
 
 namespace Jellyfin.Extensions
 {
@@ -7,6 +11,44 @@ namespace Jellyfin.Extensions
     /// </summary>
     public static class StringExtensions
     {
+        // Matches non-conforming unicode chars
+        // https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/
+        private static readonly Regex _nonConformingUnicode = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])|(\ufffd)");
+
+        /// <summary>
+        /// Removes the diacritics character from the strings.
+        /// </summary>
+        /// <param name="text">The string to act on.</param>
+        /// <returns>The string without diacritics character.</returns>
+        public static string RemoveDiacritics(this string text)
+        {
+            string withDiactritics = _nonConformingUnicode
+                .Replace(text, string.Empty)
+                .Normalize(NormalizationForm.FormD);
+
+            var withoutDiactritics = new StringBuilder();
+            foreach (char c in withDiactritics)
+            {
+                UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(c);
+                if (uc != UnicodeCategory.NonSpacingMark)
+                {
+                    withoutDiactritics.Append(c);
+                }
+            }
+
+            return withoutDiactritics.ToString().Normalize(NormalizationForm.FormC);
+        }
+
+        /// <summary>
+        /// Checks wether or not the specified string has diacritics in it.
+        /// </summary>
+        /// <param name="text">The string to check.</param>
+        /// <returns>True if the string has diacritics, false otherwise.</returns>
+        public static bool HasDiacritics(this string text)
+        {
+            return !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
+        }
+
         /// <summary>
         /// Counts the number of occurrences of [needle] in the string.
         /// </summary>

+ 32 - 0
tests/Jellyfin.Extensions.Tests/StringExtensionsTests.cs

@@ -5,6 +5,38 @@ namespace Jellyfin.Extensions.Tests
 {
     public class StringExtensionsTests
     {
+        [Theory]
+        [InlineData("", "")] // Identity edge-case (no diactritics)
+        [InlineData("Indiana Jones", "Indiana Jones")] // Identity (no diactritics)
+        [InlineData("a\ud800b", "ab")] // Invalid UTF-16 char stripping
+        [InlineData("Jön", "Jon")] // Issue #7484
+        [InlineData("Jönssonligan", "Jonssonligan")] // Issue #7484
+        [InlineData("Kieślowski", "Kieslowski")] // Issue #7450
+        [InlineData("Cidadão Kane", "Cidadao Kane")] // Issue #7560
+        [InlineData("운명처럼 널 사랑해", "운명처럼 널 사랑해")] // Issue #6393 (Korean language support)
+        [InlineData("애타는 로맨스", "애타는 로맨스")] // Issue #6393
+        public void RemoveDiacritics_ValidInput_Corrects(string input, string expectedResult)
+        {
+            string result = input.RemoveDiacritics();
+            Assert.Equal(expectedResult, result);
+        }
+
+        [Theory]
+        [InlineData("", false)] // Identity edge-case (no diactritics)
+        [InlineData("Indiana Jones", false)] // Identity (no diactritics)
+        [InlineData("a\ud800b", true)] // Invalid UTF-16 char stripping
+        [InlineData("Jön", true)] // Issue #7484
+        [InlineData("Jönssonligan", true)] // Issue #7484
+        [InlineData("Kieślowski", true)] // Issue #7450
+        [InlineData("Cidadão Kane", true)] // Issue #7560
+        [InlineData("운명처럼 널 사랑해", false)] // Issue #6393 (Korean language support)
+        [InlineData("애타는 로맨스", false)] // Issue #6393
+        public void HasDiacritics_ValidInput_Corrects(string input, bool expectedResult)
+        {
+            bool result = input.HasDiacritics();
+            Assert.Equal(expectedResult, result);
+        }
+
         [Theory]
         [InlineData("", '_', 0)]
         [InlineData("___", '_', 3)]

+ 28 - 21
tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs

@@ -27,7 +27,7 @@ namespace Jellyfin.Model.Tests
         [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
         [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
         [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
-        [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+        [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
         [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
         [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
         // Firefox
@@ -38,7 +38,7 @@ namespace Jellyfin.Model.Tests
         [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
         [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
         [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
-        [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+        [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
         [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
         [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
         // Safari
@@ -89,7 +89,7 @@ namespace Jellyfin.Model.Tests
         [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
         [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")]
         [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")]
-        [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+        [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
         [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
         [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
         // TranscodeMedia
@@ -177,7 +177,7 @@ namespace Jellyfin.Model.Tests
         [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
         [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
         [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
-        [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+        [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
         [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
         [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
         // Firefox
@@ -187,7 +187,7 @@ namespace Jellyfin.Model.Tests
         [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
         [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
         [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
-        [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
+        [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
         [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
         [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
         // Safari
@@ -338,23 +338,23 @@ namespace Jellyfin.Model.Tests
             Assert.NotNull(mediaSource);
             var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video);
             var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio);
-            // TODO: check AudioStreamIndex vs options.AudioStreamIndex
+            // TODO: Check AudioStreamIndex vs options.AudioStreamIndex
             var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex);
 
             var uri = ParseUri(val);
 
             if (playMethod == PlayMethod.DirectPlay)
             {
-                // check expected container
+                // Check expected container
                 var containers = ContainerProfile.SplitValue(mediaSource.Container);
-                // TODO: test transcode too
+                // TODO: Test transcode too
                 // Assert.Contains(uri.Extension, containers);
 
-                // check expected video codec (1)
+                // Check expected video codec (1)
                 Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
                 Assert.Single(val.TargetVideoCodec);
 
-                // check expected audio codecs (1)
+                // Check expected audio codecs (1)
                 Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec);
                 Assert.Single(val.TargetAudioCodec);
                 // Assert.Single(val.AudioCodecs);
@@ -370,7 +370,7 @@ namespace Jellyfin.Model.Tests
                 Assert.NotEmpty(val.VideoCodecs);
                 Assert.NotEmpty(val.AudioCodecs);
 
-                // check expected container (todo: this could be a test param)
+                // Check expected container (todo: this could be a test param)
                 if (transcodeProtocol == "http")
                 {
                     // Assert.Equal("webm", val.Container);
@@ -403,32 +403,39 @@ namespace Jellyfin.Model.Tests
                             stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs));
                     }
 
-                    // todo: fill out tests here
+                    // TODO: Fill out tests here
                 }
 
                 // DirectStream and Remux
                 else
                 {
-                    // check expected video codec (1)
+                    // Check expected video codec (1)
                     Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
                     Assert.Single(val.TargetVideoCodec);
 
                     if (transcodeMode == "DirectStream")
                     {
+                        // Check expected audio codecs (1)
                         if (!targetAudioStream.IsExternal)
                         {
-                            // check expected audio codecs (1)
-                            Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
+                            if (val.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported))
+                            {
+                                Assert.Contains(targetAudioStream.Codec, val.AudioCodecs);
+                            }
+                            else
+                            {
+                                Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
+                            }
                         }
                     }
                     else if (transcodeMode == "Remux")
                     {
-                        // check expected audio codecs (1)
+                        // Check expected audio codecs (1)
                         Assert.Contains(targetAudioStream.Codec, val.AudioCodecs);
                         Assert.Single(val.AudioCodecs);
                     }
 
-                    // video details
+                    // Video details
                     var videoStream = targetVideoStream;
                     Assert.False(val.EstimateContentLength);
                     Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo);
@@ -437,10 +444,10 @@ namespace Jellyfin.Model.Tests
                     Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth);
                     Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue);
 
-                    // audio codec not supported
+                    // Audio codec not supported
                     if ((why & TranscodeReason.AudioCodecNotSupported) != 0)
                     {
-                        // audio stream specified
+                        // Audio stream specified
                         if (options.AudioStreamIndex >= 0)
                         {
                             // TODO:fixme
@@ -450,10 +457,10 @@ namespace Jellyfin.Model.Tests
                             }
                         }
 
-                        // audio stream not specified
+                        // Audio stream not specified
                         else
                         {
-                            // TODO:fixme
+                            // TODO: Fixme
                             Assert.All(audioStreams, stream =>
                             {
                                 if (!stream.IsExternal)

+ 4 - 4
tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json

@@ -45,8 +45,8 @@
         },
         {
             "Container": "wmv",
-            "AudioCodec": "",
-            "VideoCodec": "",
+            "AudioCodec": "wma",
+            "VideoCodec": "wmv,vc1",
             "Type": "Video",
             "$type": "DirectPlayProfile"
         },
@@ -59,8 +59,8 @@
         },
         {
             "Container": "asf",
-            "AudioCodec": "",
-            "VideoCodec": "",
+            "AudioCodec": "wma",
+            "VideoCodec": "wmv,vc1",
             "Type": "Video",
             "$type": "DirectPlayProfile"
         },

+ 4 - 4
tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json

@@ -45,8 +45,8 @@
         },
         {
             "Container": "wmv",
-            "AudioCodec": "",
-            "VideoCodec": "",
+            "AudioCodec": "wma",
+            "VideoCodec": "wmv,vc1",
             "Type": "Video",
             "$type": "DirectPlayProfile"
         },
@@ -59,8 +59,8 @@
         },
         {
             "Container": "asf",
-            "AudioCodec": "",
-            "VideoCodec": "",
+            "AudioCodec": "wma",
+            "VideoCodec": "wmv,vc1",
             "Type": "Video",
             "$type": "DirectPlayProfile"
         },

+ 2 - 1
tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs

@@ -1,4 +1,4 @@
-using Emby.Naming.Common;
+using Emby.Naming.Common;
 using Emby.Naming.TV;
 using Xunit;
 
@@ -9,6 +9,7 @@ namespace Jellyfin.Naming.Tests.TV
         private readonly NamingOptions _namingOptions = new NamingOptions();
 
         [Theory]
+        [InlineData("Season 21/One Piece 1001", 1001)]
         [InlineData("Watchmen (2019)/Watchmen 1x03 [WEBDL-720p][EAC3 5.1][h264][-TBS] - She Was Killed by Space Junk.mkv", 3)]
         [InlineData("The Daily Show/The Daily Show 25x22 - [WEBDL-720p][AAC 2.0][x264] Noah Baumbach-TBS.mkv", 22)]
         [InlineData("Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", 1)]

+ 11 - 0
tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs

@@ -85,6 +85,17 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv
                     EpisodeTitle = "The VCR Illumination"
                 });
 
+            data.Add(
+               "Lorem ipsum dolor sit amet: consect 2018_12_06_21_06_00",
+               new TimerInfo
+               {
+                   Name = "Lorem ipsum dolor sit amet: consect",
+                   IsProgramSeries = true,
+                   StartDate = new DateTime(2018, 12, 6, 21, 6, 0, DateTimeKind.Local),
+                   OriginalAirDate = new DateTime(2018, 12, 6),
+                   EpisodeTitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor"
+               });
+
             return data;
         }