Sfoglia il codice sorgente

support track selection before playback

Luke Pulverenti 7 anni fa
parent
commit
5cb7469028
41 ha cambiato i file con 353 aggiunte e 588 eliminazioni
  1. 0 1
      Emby.Dlna/DlnaManager.cs
  2. 0 2
      Emby.Dlna/Emby.Dlna.csproj
  3. 4 2
      Emby.Dlna/Profiles/DefaultProfile.cs
  4. 0 326
      Emby.Dlna/Profiles/Xbox360Profile.cs
  5. 2 2
      Emby.Dlna/Profiles/XboxOneProfile.cs
  6. 2 2
      Emby.Dlna/Profiles/Xml/Default.xml
  7. 0 31
      Emby.Dlna/Profiles/Xml/Xbox 360.xml
  8. 2 2
      Emby.Dlna/Profiles/Xml/Xbox One.xml
  9. 1 1
      Emby.Server.Implementations/ApplicationHost.cs
  10. 2 14
      Emby.Server.Implementations/Channels/ChannelManager.cs
  11. 2 0
      Emby.Server.Implementations/Dto/DtoService.cs
  12. 31 11
      Emby.Server.Implementations/IO/SharpCifsFileSystem.cs
  13. 1 1
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  14. 4 0
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  15. 11 11
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  16. 2 3
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHttpStream.cs
  17. 3 2
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  18. 10 1
      Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
  19. 23 23
      Emby.Server.Implementations/Localization/Core/bg-BG.json
  20. 18 18
      Emby.Server.Implementations/Localization/Core/sk.json
  21. 1 1
      Emby.Server.Implementations/Localization/Core/sv.json
  22. 20 8
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  23. 5 4
      Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
  24. 4 1
      Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs
  25. 5 0
      MediaBrowser.Controller/Entities/Audio/Audio.cs
  26. 1 1
      MediaBrowser.Controller/Entities/BaseItem.cs
  27. 1 0
      MediaBrowser.Controller/Entities/IHasMediaSources.cs
  28. 5 0
      MediaBrowser.Controller/Entities/Video.cs
  29. 5 0
      MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
  30. 9 7
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  31. 2 1
      MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs
  32. 28 24
      MediaBrowser.Controller/Providers/DirectoryService.cs
  33. 3 0
      MediaBrowser.Controller/Providers/IDirectoryService.cs
  34. 2 1
      MediaBrowser.Model/Dto/BaseItemDto.cs
  35. 2 0
      MediaBrowser.Model/Providers/SubtitleOptions.cs
  36. 2 1
      MediaBrowser.Model/Session/GeneralCommandType.cs
  37. 4 2
      MediaBrowser.Providers/Manager/MetadataService.cs
  38. 3 3
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  39. 66 49
      MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
  40. 66 31
      MediaBrowser.Providers/Subtitles/SubtitleManager.cs
  41. 1 1
      SharedVersion.cs

+ 0 - 1
Emby.Dlna/DlnaManager.cs

@@ -554,7 +554,6 @@ namespace Emby.Dlna
             var list = new List<DeviceProfile>
             {
                 new SamsungSmartTvProfile(),
-                new Xbox360Profile(),
                 new XboxOneProfile(),
                 new SonyPs3Profile(),
                 new SonyPs4Profile(),

+ 0 - 2
Emby.Dlna/Emby.Dlna.csproj

@@ -105,7 +105,6 @@
     <Compile Include="Profiles\SonyPs3Profile.cs" />
     <Compile Include="Profiles\SonyPs4Profile.cs" />
     <Compile Include="Profiles\WdtvLiveProfile.cs" />
-    <Compile Include="Profiles\Xbox360Profile.cs" />
     <Compile Include="Profiles\XboxOneProfile.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Server\DescriptionXmlBuilder.cs" />
@@ -175,7 +174,6 @@
     <EmbeddedResource Include="Profiles\Xml\Sony PlayStation 3.xml" />
     <EmbeddedResource Include="Profiles\Xml\Sony PlayStation 4.xml" />
     <EmbeddedResource Include="Profiles\Xml\WDTV Live.xml" />
-    <EmbeddedResource Include="Profiles\Xml\Xbox 360.xml" />
     <EmbeddedResource Include="Profiles\Xml\Xbox One.xml" />
   </ItemGroup>
   <ItemGroup>

+ 4 - 2
Emby.Dlna/Profiles/DefaultProfile.cs

@@ -65,13 +65,15 @@ namespace Emby.Dlna.Profiles
             {
                 new DirectPlayProfile
                 {
-                    Container = "m4v,mpegts,ts,3gp,mov,xvid,vob,mkv,wmv,asf,ogm,ogv,m2v,avi,mpg,mpeg,mp4,webm,wtv,m2ts,dvr-ms",
+                    // play all
+                    Container = "",
                     Type = DlnaProfileType.Video
                 },
 
                 new DirectPlayProfile
                 {
-                    Container = "aac,mp3,mpa,wav,wma,mp2,ogg,oga,webma,ape,opus,flac,m4a",
+                    // play all
+                    Container = "",
                     Type = DlnaProfileType.Audio
                 }
             };

+ 0 - 326
Emby.Dlna/Profiles/Xbox360Profile.cs

@@ -1,326 +0,0 @@
-using MediaBrowser.Model.Dlna;
-using System.Xml.Serialization;
-
-namespace Emby.Dlna.Profiles
-{
-    /// <summary>
-    /// Good info on xbox 360 requirements: https://code.google.com/p/jems/wiki/XBox360Notes
-    /// </summary>
-    [XmlRoot("Profile")]
-    public class Xbox360Profile : DefaultProfile
-    {
-        public Xbox360Profile()
-        {
-            Name = "Xbox 360";
-
-            // Required according to above
-            ModelName = "Windows Media Player Sharing";
-
-            ModelNumber = "12.0";
-
-            FriendlyName = "${HostName}: 1";
-
-            ModelUrl = "http://go.microsoft.com/fwlink/?LinkId=105926";
-            Manufacturer = "Microsoft Corporation";
-            ManufacturerUrl = "http://www.microsoft.com";
-            XDlnaDoc = "DMS-1.50";
-            ModelDescription = "Emby : UPnP Media Server";
-
-            TimelineOffsetSeconds = 40;
-            RequiresPlainFolders = true;
-            RequiresPlainVideoItems = true;
-            EnableMSMediaReceiverRegistrar = true;
-
-            Identification = new DeviceIdentification
-            {
-                ModelName = "Xbox 360",
-
-                Headers = new[]
-                {
-                    new HttpHeaderInfo {Name = "User-Agent", Value = "Xbox", Match = HeaderMatchType.Substring},
-                    new HttpHeaderInfo {Name = "User-Agent", Value = "Xenon", Match = HeaderMatchType.Substring}
-                }
-            };
-
-            TranscodingProfiles = new[]
-            {
-                new TranscodingProfile
-                {
-                    Container = "mp3",
-                    AudioCodec = "mp3",
-                    Type = DlnaProfileType.Audio
-                },
-                new TranscodingProfile
-                {
-                    Container = "asf",
-                    VideoCodec = "wmv2",
-                    AudioCodec = "wmav2",
-                    Type = DlnaProfileType.Video,
-                    TranscodeSeekInfo = TranscodeSeekInfo.Bytes,
-                    EstimateContentLength = true
-                },
-                new TranscodingProfile
-                {
-                    Container = "jpeg",
-                    Type = DlnaProfileType.Photo
-                }
-            };
-
-            DirectPlayProfiles = new[]
-            {
-                new DirectPlayProfile
-                {
-                    Container = "avi",
-                    VideoCodec = "mpeg4",
-                    AudioCodec = "ac3,mp3",
-                    Type = DlnaProfileType.Video
-                },
-                new DirectPlayProfile
-                {
-                    Container = "avi",
-                    VideoCodec = "h264",
-                    AudioCodec = "aac",
-                    Type = DlnaProfileType.Video
-                },
-                new DirectPlayProfile
-                {
-                    Container = "mp4,mov",
-                    VideoCodec = "h264,mpeg4",
-                    AudioCodec = "aac,ac3",
-                    Type = DlnaProfileType.Video
-                },
-                new DirectPlayProfile
-                {
-                    Container = "asf",
-                    VideoCodec = "wmv2,wmv3,vc1",
-                    AudioCodec = "wmav2,wmapro",
-                    Type = DlnaProfileType.Video
-                },
-                new DirectPlayProfile
-                {
-                    Container = "asf",
-                    AudioCodec = "wmav2,wmapro,wmavoice",
-                    Type = DlnaProfileType.Audio
-                },
-                new DirectPlayProfile
-                {
-                    Container = "mp3",
-                    AudioCodec = "mp3",
-                    Type = DlnaProfileType.Audio
-                },
-                new DirectPlayProfile
-                {
-                    Container = "jpeg",
-                    Type = DlnaProfileType.Photo
-                }
-            };
-
-            ResponseProfiles = new[]
-            {
-                new ResponseProfile
-                {
-                    Container = "avi",
-                    MimeType = "video/avi",
-                    Type = DlnaProfileType.Video
-                }
-            };
-
-            ContainerProfiles = new[]
-            {
-                new ContainerProfile
-                {
-                    Type = DlnaProfileType.Video,
-                    Container = "mp4,mov",
-
-                    Conditions = new []
-                    {
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.Equals,
-                            Property = ProfileConditionValue.Has64BitOffsets,
-                            Value = "false",
-                            IsRequired = false
-                        }
-                    }
-                },
-
-                new ContainerProfile
-                {
-                    Type = DlnaProfileType.Photo,
-
-                    Conditions = new []
-                    {
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.Width,
-                            Value = "1920"
-                        },
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.Height,
-                            Value = "1080"
-                        }
-                    }
-                }
-            };
-
-            CodecProfiles = new[]
-            {
-                new CodecProfile
-                {
-                    Type = CodecType.Video,
-                    Codec = "mpeg4",
-                    Conditions = new []
-                    {
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.Width,
-                            Value = "1280"
-                        },
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.Height,
-                            Value = "720"
-                        },
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.VideoFramerate,
-                            Value = "30",
-                            IsRequired = false
-                        },
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.VideoBitrate,
-                            Value = "5120000",
-                            IsRequired = false
-                        }
-                    }
-                },
-
-                new CodecProfile
-                {
-                    Type = CodecType.Video,
-                    Codec = "h264",
-                    Conditions = new []
-                    {
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.Width,
-                            Value = "1920"
-                        },
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.Height,
-                            Value = "1080"
-                        },
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.VideoLevel,
-                            Value = "41",
-                            IsRequired = false
-                        },
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.VideoBitrate,
-                            Value = "10240000",
-                            IsRequired = false
-                        }
-                    }
-                },
-
-                new CodecProfile
-                {
-                    Type = CodecType.Video,
-                    Codec = "wmv2,wmv3,vc1",
-                    Conditions = new []
-                    {
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.Width,
-                            Value = "1920"
-                        },
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.Height,
-                            Value = "1080"
-                        },
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.VideoFramerate,
-                            Value = "30",
-                            IsRequired = false
-                        },
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.VideoBitrate,
-                            Value = "15360000",
-                            IsRequired = false
-                        }
-                    }
-                },
-
-                new CodecProfile
-                {
-                    Type = CodecType.VideoAudio,
-                    Codec = "ac3,wmav2,wmapro",
-                    Conditions = new []
-                    {
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.AudioChannels,
-                            Value = "6",
-                            IsRequired = false
-                        }
-                    }
-                },
-
-                new CodecProfile
-                {
-                    Type = CodecType.VideoAudio,
-                    Codec = "aac",
-                    Conditions = new []
-                    {
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.LessThanEqual,
-                            Property = ProfileConditionValue.AudioChannels,
-                            Value = "2",
-                            IsRequired = false
-                        },
-                        new ProfileCondition
-                        {
-                            Condition = ProfileConditionType.Equals,
-                            Property = ProfileConditionValue.AudioProfile,
-                            Value = "lc",
-                            IsRequired = false
-                        }
-                    }
-                }
-            };
-
-            SubtitleProfiles = new[]
-            {
-                new SubtitleProfile
-                {
-                    Format = "srt",
-                    Method = SubtitleDeliveryMethod.Embed
-                }
-            };
-        }
-    }
-}

+ 2 - 2
Emby.Dlna/Profiles/XboxOneProfile.cs

@@ -60,7 +60,7 @@ namespace Emby.Dlna.Profiles
                 new DirectPlayProfile
                 {
                     Container = "ts",
-                    VideoCodec = "h264,mpeg2video",
+                    VideoCodec = "h264,mpeg2video,hevc",
                     AudioCodec = "ac3,aac,mp3",
                     Type = DlnaProfileType.Video
                 },
@@ -81,7 +81,7 @@ namespace Emby.Dlna.Profiles
                 new DirectPlayProfile
                 {
                     Container = "mp4,mov,mkv,m4v",
-                    VideoCodec = "h264,mpeg4,mpeg2video",
+                    VideoCodec = "h264,mpeg4,mpeg2video,hevc",
                     AudioCodec = "aac,ac3",
                     Type = DlnaProfileType.Video
                 },

+ 2 - 2
Emby.Dlna/Profiles/Xml/Default.xml

@@ -29,8 +29,8 @@
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <XmlRootAttributes />
   <DirectPlayProfiles>
-    <DirectPlayProfile container="m4v,mpegts,ts,3gp,mov,xvid,vob,mkv,wmv,asf,ogm,ogv,m2v,avi,mpg,mpeg,mp4,webm,wtv,m2ts,dvr-ms" type="Video" />
-    <DirectPlayProfile container="aac,mp3,mpa,wav,wma,mp2,ogg,oga,webma,ape,opus,flac,m4a" type="Audio" />
+    <DirectPlayProfile container="" type="Video" />
+    <DirectPlayProfile container="" type="Audio" />
   </DirectPlayProfiles>
   <TranscodingProfiles>
     <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />

File diff suppressed because it is too large
+ 0 - 31
Emby.Dlna/Profiles/Xml/Xbox 360.xml


+ 2 - 2
Emby.Dlna/Profiles/Xml/Xbox One.xml

@@ -36,10 +36,10 @@
   <IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
   <XmlRootAttributes />
   <DirectPlayProfiles>
-    <DirectPlayProfile container="ts" audioCodec="ac3,aac,mp3" videoCodec="h264,mpeg2video" type="Video" />
+    <DirectPlayProfile container="ts" audioCodec="ac3,aac,mp3" videoCodec="h264,mpeg2video,hevc" type="Video" />
     <DirectPlayProfile container="avi" audioCodec="ac3,mp3" videoCodec="mpeg4" type="Video" />
     <DirectPlayProfile container="avi" audioCodec="aac" videoCodec="h264" type="Video" />
-    <DirectPlayProfile container="mp4,mov,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4,mpeg2video" type="Video" />
+    <DirectPlayProfile container="mp4,mov,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4,mpeg2video,hevc" type="Video" />
     <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro" videoCodec="wmv2,wmv3,vc1" type="Video" />
     <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
     <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" />

+ 1 - 1
Emby.Server.Implementations/ApplicationHost.cs

@@ -996,7 +996,7 @@ namespace Emby.Server.Implementations
             NotificationManager = new NotificationManager(LogManager, UserManager, ServerConfigurationManager);
             RegisterSingleInstance(NotificationManager);
 
-            SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor, LibraryManager, MediaSourceManager);
+            SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor, LibraryManager, MediaSourceManager, ServerConfigurationManager);
             RegisterSingleInstance(SubtitleManager);
 
             RegisterSingleInstance<IDeviceDiscovery>(new DeviceDiscovery(LogManager.GetLogger("IDeviceDiscovery"), ServerConfigurationManager, SocketFactory, TimerFactory));

+ 2 - 14
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.Channels
 
             if (requiresCallback != null)
             {
-                results = await GetChannelItemMediaSourcesInternal(requiresCallback, GetItemExternalId(item), cancellationToken)
+                results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken)
                     .ConfigureAwait(false);
             }
             else
@@ -990,18 +990,6 @@ namespace Emby.Server.Implementations.Channels
             return result;
         }
 
-        private string GetItemExternalId(BaseItem item)
-        {
-            var externalId = item.ExternalId;
-
-            if (string.IsNullOrWhiteSpace(externalId))
-            {
-                externalId = item.GetProviderId("ProviderExternalId");
-            }
-
-            return externalId;
-        }
-
         private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
         private async Task<ChannelItemResult> GetChannelItems(IChannel channel,
             User user,
@@ -1080,7 +1068,7 @@ namespace Emby.Server.Implementations.Channels
                 {
                     var categoryItem = _libraryManager.GetItemById(new Guid(folderId));
 
-                    query.FolderId = GetItemExternalId(categoryItem);
+                    query.FolderId = categoryItem.ExternalId;
                 }
 
                 var result = await channel.GetChannelItems(query, cancellationToken).ConfigureAwait(false);

+ 2 - 0
Emby.Server.Implementations/Dto/DtoService.cs

@@ -373,6 +373,8 @@ namespace Emby.Server.Implementations.Dto
                     }
 
                     NormalizeMediaSourceContainers(dto);
+
+                    dto.SupportsMediaSourceSelection = hasMediaSources.SupportsMediaSourceSelection();
                 }
             }
 

+ 31 - 11
Emby.Server.Implementations/IO/SharpCifsFileSystem.cs

@@ -487,14 +487,17 @@ namespace Emby.Server.Implementations.IO
             AssertDirectoryExists(dir, path);
 
             var list = ListFiles(dir, recursive);
+            var result = new List<FileSystemMetadata>();
 
             foreach (var file in list)
             {
                 if (file.IsDirectory())
                 {
-                    yield return ToMetadata(file);
+                    result.Add(ToMetadata(file));
                 }
             }
+
+            return result;
         }
 
         public IEnumerable<FileSystemMetadata> GetFiles(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
@@ -503,6 +506,7 @@ namespace Emby.Server.Implementations.IO
             AssertDirectoryExists(dir, path);
 
             var list = ListFiles(dir, recursive);
+            var result = new List<FileSystemMetadata>();
 
             foreach (var file in list)
             {
@@ -513,10 +517,12 @@ namespace Emby.Server.Implementations.IO
 
                     if (extensions == null || extensions.Length == 0 || extensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
                     {
-                        yield return ToMetadata(file);
+                        result.Add(ToMetadata(file));
                     }
                 }
             }
+
+            return result;
         }
 
         public IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
@@ -525,15 +531,19 @@ namespace Emby.Server.Implementations.IO
             AssertDirectoryExists(dir, path);
 
             var list = ListFiles(dir, recursive);
+            var result = new List<FileSystemMetadata>();
 
             foreach (var file in list)
             {
-                yield return ToMetadata(file);
+                result.Add(ToMetadata(file));
             }
+
+            return result;
         }
 
-        public IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
+        public List<string> GetFileSystemEntryPaths(string path, bool recursive = false)
         {
+            var result = new List<string>();
             var dir = CreateSmbDirectoryForListFiles(path);
             AssertDirectoryExists(dir, path);
 
@@ -541,16 +551,18 @@ namespace Emby.Server.Implementations.IO
 
             foreach (var file in list)
             {
-                yield return GetReturnPath(file);
+                result.Add(GetReturnPath(file));
             }
+            return result;
         }
 
-        public IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
+        public List<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
         {
             var dir = CreateSmbDirectoryForListFiles(path);
             AssertDirectoryExists(dir, path);
 
             var list = ListFiles(dir, recursive);
+            var result = new List<string>();
 
             foreach (var file in list)
             {
@@ -561,44 +573,52 @@ namespace Emby.Server.Implementations.IO
 
                     if (extensions == null || extensions.Length == 0 || extensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
                     {
-                        yield return filePath;
+                        result.Add(filePath);
                     }
                 }
             }
+
+            return result;
         }
 
-        public IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
+        public List<string> GetDirectoryPaths(string path, bool recursive = false)
         {
             var dir = CreateSmbDirectoryForListFiles(path);
             AssertDirectoryExists(dir, path);
 
             var list = ListFiles(dir, recursive);
+            var result = new List<string>();
 
             foreach (var file in list)
             {
                 if (file.IsDirectory())
                 {
-                    yield return GetReturnPath(file);
+                    result.Add(GetReturnPath(file));
                 }
             }
+
+            return result;
         }
 
         private IEnumerable<SmbFile> ListFiles(SmbFile dir, bool recursive)
         {
             var list = dir.ListFiles();
+            var result = new List<SmbFile>();
 
             foreach (var file in list)
             {
-                yield return file;
+                result.Add(file);
 
                 if (recursive && file.IsDirectory())
                 {
                     foreach (var subFile in ListFiles(file, recursive))
                     {
-                        yield return subFile;
+                        result.Add(subFile);
                     }
                 }
             }
+
+            return result;
         }
     }
 }

+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -274,7 +274,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 return false;
             }
 
-            return FileSystem.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
+            return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
         }
 
         /// <summary>

+ 4 - 0
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -2445,6 +2445,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                             {
                                 existingTimer.Status = RecordingStatus.Cancelled;
                             }
+                            else if (!existingTimer.IsManual)
+                            {
+                                existingTimer.Status = RecordingStatus.New;
+                            }
 
                             if (existingTimer.Status != RecordingStatus.Cancelled)
                             {

+ 11 - 11
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -1232,6 +1232,8 @@ namespace Emby.Server.Implementations.LiveTv
             var newChannelIdList = new List<Guid>();
             var newProgramIdList = new List<Guid>();
 
+            var cleanDatabase = true;
+
             foreach (var service in _services)
             {
                 cancellationToken.ThrowIfCancellationRequested();
@@ -1254,6 +1256,7 @@ namespace Emby.Server.Implementations.LiveTv
                 }
                 catch (Exception ex)
                 {
+                    cleanDatabase = false;
                     _logger.ErrorException("Error refreshing channels for service", ex);
                 }
 
@@ -1264,8 +1267,11 @@ namespace Emby.Server.Implementations.LiveTv
                 progress.Report(100 * percent);
             }
 
-            await CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken).ConfigureAwait(false);
-            await CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken).ConfigureAwait(false);
+            if (cleanDatabase)
+            {
+                await CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken).ConfigureAwait(false);
+                await CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken).ConfigureAwait(false);
+            }
 
             var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
 
@@ -1291,8 +1297,9 @@ namespace Emby.Server.Implementations.LiveTv
         {
             progress.Report(10);
 
-            var allChannels = await GetChannels(service, cancellationToken).ConfigureAwait(false);
-            var allChannelsList = allChannels.ToList();
+            var allChannelsList = (await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false))
+                .Select(i => new Tuple<string, ChannelInfo>(service.Name, i))
+                .ToList();
 
             var list = new List<LiveTvChannel>();
 
@@ -1507,13 +1514,6 @@ namespace Emby.Server.Implementations.LiveTv
             return 7;
         }
 
-        private async Task<IEnumerable<Tuple<string, ChannelInfo>>> GetChannels(ILiveTvService service, CancellationToken cancellationToken)
-        {
-            var channels = await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false);
-
-            return channels.Select(i => new Tuple<string, ChannelInfo>(service.Name, i));
-        }
-
         private DateTime _lastRecordingRefreshTime;
         private async Task RefreshRecordings(Guid internalLiveTvFolderId, CancellationToken cancellationToken)
         {

+ 2 - 3
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHttpStream.cs

@@ -73,9 +73,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             //OpenedMediaSource.SupportsTranscoding = true;
         }
 
-        public override void Close()
+        protected override void CloseInternal()
         {
-            Logger.Info("Closing HDHR live stream");
             LiveStreamCancellationTokenSource.Cancel();
         }
 
@@ -106,7 +105,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                 {
                     Logger.ErrorException("Error copying live stream.", ex);
                 }
-
+                EnableStreamSharing = false;
                 await DeleteTempFile(TempFilePath).ConfigureAwait(false);
             });
         }

+ 3 - 2
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs

@@ -105,9 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             await taskCompletionSource.Task.ConfigureAwait(false);
         }
 
-        public override void Close()
+        protected override void CloseInternal()
         {
-            Logger.Info("Closing HDHR UDP live stream");
             LiveStreamCancellationTokenSource.Cancel();
         }
 
@@ -134,6 +133,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                             openTaskCompletionSource.TrySetException(ex);
                         }
 
+                        EnableStreamSharing = false;
+
                         try
                         {
                             await hdHomerunManager.StopStreaming().ConfigureAwait(false);

+ 10 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs

@@ -52,7 +52,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             return Task.FromResult(true);
         }
 
-        public virtual void Close()
+        public void Close()
+        {
+            EnableStreamSharing = false;
+
+            Logger.Info("Closing " + GetType().Name);
+
+            CloseInternal();
+        }
+
+        protected virtual void CloseInternal()
         {
         }
 

+ 23 - 23
Emby.Server.Implementations/Localization/Core/bg-BG.json

@@ -1,7 +1,7 @@
 {
     "Latest": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438",
     "ValueSpecialEpisodeName": "\u0421\u043f\u0435\u0446\u0438\u0430\u043b\u043d\u0438 - {0}",
-    "Inherit": "Inherit",
+    "Inherit": "\u041d\u0430\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435",
     "Books": "\u041a\u043d\u0438\u0433\u0438",
     "Music": "\u041c\u0443\u0437\u0438\u043a\u0430",
     "Games": "\u0418\u0433\u0440\u0438",
@@ -35,7 +35,7 @@
     "UserDownloadingItemWithValues": "{0} is downloading {1}",
     "HeaderLiveTV": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u0438\u044f \u043d\u0430 \u0436\u0438\u0432\u043e",
     "ChapterNameValue": "\u0413\u043b\u0430\u0432\u0430 {0}",
-    "ScheduledTaskFailedWithName": "{0} failed",
+    "ScheduledTaskFailedWithName": "{0} \u0441\u0435 \u043f\u0440\u043e\u0432\u0430\u043b\u0438",
     "LabelRunningTimeValue": "Running time: {0}",
     "ScheduledTaskStartedWithName": "{0} \u0437\u0430\u043f\u043e\u0447\u043d\u0430",
     "VersionNumber": "\u0412\u0435\u0440\u0441\u0438\u044f {0}",
@@ -49,40 +49,40 @@
     "DeviceOnlineWithName": "{0} \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d",
     "UserOnlineFromDevice": "{0} \u0435 \u043d\u0430 \u043b\u0438\u043d\u0438\u044f \u043e\u0442 {1}",
     "ProviderValue": "\u0414\u043e\u0441\u0442\u0430\u0432\u0447\u0438\u043a: {0}",
-    "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
-    "UserCreatedWithName": "User {0} has been created",
-    "UserPasswordChangedWithName": "Password has been changed for user {0}",
-    "UserDeletedWithName": "User {0} has been deleted",
+    "SubtitlesDownloadedForItem": "\u0418\u0437\u0442\u0435\u0433\u043b\u0435\u043d\u0438 \u0441\u0430 \u0441\u0443\u0431\u0442\u0438\u0442\u0440\u0438 \u0437\u0430 {0}",
+    "UserCreatedWithName": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 {0} \u0435 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u043d",
+    "UserPasswordChangedWithName": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f {0} \u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0435\u043d\u0430",
+    "UserDeletedWithName": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 {0} \u0435 \u0438\u0437\u0442\u0440\u0438\u0442",
     "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}",
     "MessageServerConfigurationUpdated": "Server configuration has been updated",
     "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
-    "MessageApplicationUpdated": "Emby Server has been updated",
+    "MessageApplicationUpdated": "\u0421\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u0435 \u043e\u0431\u043d\u043e\u0432\u0435\u043d",
     "FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
-    "AuthenticationSucceededWithUserName": "{0} successfully authenticated",
-    "UserOfflineFromDevice": "{0} has disconnected from {1}",
-    "DeviceOfflineWithName": "{0} has disconnected",
+    "AuthenticationSucceededWithUserName": "{0} \u0441\u0435 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0438 \u0443\u0441\u043f\u0435\u0448\u043d\u043e",
+    "UserOfflineFromDevice": "{0} \u0441\u0435 \u0440\u0430\u0437\u043a\u0430\u0447\u0438 \u043e\u0442 {1}",
+    "DeviceOfflineWithName": "{0} \u0441\u0435 \u0440\u0430\u0437\u043a\u0430\u0447\u0438",
     "UserStartedPlayingItemWithValues": "{0} has started playing {1}",
     "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}",
-    "NotificationOptionPluginError": "Plugin failure",
-    "NotificationOptionApplicationUpdateAvailable": "Application update available",
-    "NotificationOptionApplicationUpdateInstalled": "Application update installed",
+    "NotificationOptionPluginError": "\u0413\u0440\u0435\u0448\u043a\u0430 \u0432 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430",
+    "NotificationOptionApplicationUpdateAvailable": "\u041d\u0430\u043b\u0438\u0447\u043d\u043e \u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0430 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0430\u0442\u0430",
+    "NotificationOptionApplicationUpdateInstalled": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435\u0442\u043e \u043d\u0430 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0430\u0442\u0430 \u0435 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u043e",
     "NotificationOptionPluginUpdateInstalled": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435\u0442\u043e \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430\u0442\u0430 \u0435 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u043e",
     "NotificationOptionPluginInstalled": "\u041f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430\u0442\u0430 \u0435 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0430",
     "NotificationOptionPluginUninstalled": "\u041f\u0440\u0438\u0441\u0442\u0430\u0432\u043a\u0430\u0442\u0430 \u0435 \u0434\u0435\u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0430",
-    "NotificationOptionVideoPlayback": "Video playback started",
-    "NotificationOptionAudioPlayback": "Audio playback started",
-    "NotificationOptionGamePlayback": "Game playback started",
-    "NotificationOptionVideoPlaybackStopped": "Video playback stopped",
-    "NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
-    "NotificationOptionGamePlaybackStopped": "Game playback stopped",
-    "NotificationOptionTaskFailed": "Scheduled task failure",
-    "NotificationOptionInstallationFailed": "Installation failure",
+    "NotificationOptionVideoPlayback": "\u0412\u044a\u0437\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0436\u0434\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0432\u0438\u0434\u0435\u043e \u0437\u0430\u043f\u043e\u0447\u043d\u0430",
+    "NotificationOptionAudioPlayback": "\u0412\u044a\u0437\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0436\u0434\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0437\u0432\u0443\u043a \u0437\u0430\u043f\u043e\u0447\u043d\u0430",
+    "NotificationOptionGamePlayback": "\u0412\u044a\u0437\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0436\u0434\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0438\u0433\u0440\u0430\u0442\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0430",
+    "NotificationOptionVideoPlaybackStopped": "\u0412\u044a\u0437\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0436\u0434\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0432\u0438\u0434\u0435\u043e \u0435 \u0441\u043f\u0440\u044f\u043d\u043e",
+    "NotificationOptionAudioPlaybackStopped": "\u0412\u044a\u0437\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0436\u0434\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0437\u0432\u0443\u043a \u0435 \u0441\u043f\u0440\u044f\u043d\u043e",
+    "NotificationOptionGamePlaybackStopped": "\u0412\u044a\u0437\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0436\u0434\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0438\u0433\u0440\u0430\u0442\u0430 \u0435 \u0441\u043f\u0440\u044f\u043d\u0430",
+    "NotificationOptionTaskFailed": "\u0413\u0440\u0435\u0448\u043a\u0430 \u0432 \u043f\u043b\u0430\u043d\u0438\u0440\u0430\u043d\u0430 \u0437\u0430\u0434\u0430\u0447\u0430",
+    "NotificationOptionInstallationFailed": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0435",
     "NotificationOptionNewLibraryContent": "\u0414\u043e\u0431\u0430\u0432\u0435\u043d\u043e \u0435 \u043d\u043e\u0432\u043e \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435",
-    "NotificationOptionCameraImageUploaded": "Camera image uploaded",
+    "NotificationOptionCameraImageUploaded": "\u0418\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u043e\u0442 \u0444\u043e\u0442\u043e\u0430\u043f\u0430\u0440\u0430\u0442\u0430 \u0435 \u043a\u0430\u0447\u0435\u043d\u043e",
     "NotificationOptionUserLockedOut": "User locked out",
     "NotificationOptionServerRestartRequired": "\u041d\u0443\u0436\u043d\u043e \u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0430",
     "UserLockedOutWithName": "User {0} has been locked out",
-    "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
+    "SubtitleDownloadFailureForItem": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0438\u0437\u0442\u0435\u0433\u043b\u044f\u043d\u0435 \u043d\u0430 \u0441\u0443\u0431\u0442\u0438\u0442\u0440\u0438 \u0437\u0430 {0}",
     "Sync": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0438\u0440\u0430\u043d\u0435",
     "User": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b",
     "System": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430",

+ 18 - 18
Emby.Server.Implementations/Localization/Core/sk.json

@@ -2,13 +2,13 @@
     "Latest": "Latest",
     "ValueSpecialEpisodeName": "Special - {0}",
     "Inherit": "Inherit",
-    "Books": "Books",
-    "Music": "Music",
-    "Games": "Games",
-    "Photos": "Photos",
-    "MixedContent": "Mixed content",
-    "MusicVideos": "Music videos",
-    "HomeVideos": "Home videos",
+    "Books": "Knihy",
+    "Music": "Hudba",
+    "Games": "Hry",
+    "Photos": "Fotky",
+    "MixedContent": "Zmie\u0161an\u00fd obsah",
+    "MusicVideos": "Hudobn\u00e9 vide\u00e1",
+    "HomeVideos": "Dom\u00e1ce vide\u00e1",
     "Playlists": "Playlists",
     "HeaderRecordingGroups": "Recording Groups",
     "HeaderContinueWatching": "Continue Watching",
@@ -16,20 +16,20 @@
     "HeaderFavoriteSongs": "Ob\u013e\u00faben\u00e9 pesni\u010dky",
     "HeaderAlbumArtists": "Album Artists",
     "HeaderFavoriteAlbums": "Favorite Albums",
-    "HeaderFavoriteEpisodes": "Favorite Episodes",
+    "HeaderFavoriteEpisodes": "Ob\u013e\u00faben\u00e9 epiz\u00f3dy",
     "HeaderFavoriteShows": "Ob\u013e\u00faben\u00e9 seri\u00e1ly",
-    "HeaderNextUp": "Next Up",
+    "HeaderNextUp": "Nasleduje",
     "Favorites": "Ob\u013e\u00faben\u00e9",
-    "Collections": "Collections",
-    "Channels": "Channels",
-    "Movies": "Movies",
-    "Albums": "Albums",
-    "Artists": "Artists",
-    "Folders": "Folders",
-    "Songs": "Songs",
+    "Collections": "Zbierky",
+    "Channels": "Kan\u00e1ly",
+    "Movies": "Filmy",
+    "Albums": "Albumy",
+    "Artists": "Umelci",
+    "Folders": "Prie\u010dinky",
+    "Songs": "Skladby",
     "TvShows": "TV Shows",
     "Shows": "Series",
-    "Genres": "Genres",
+    "Genres": "\u017d\u00e1nre",
     "NameSeasonNumber": "Season {0}",
     "AppDeviceValues": "App: {0}, Device: {1}",
     "UserDownloadingItemWithValues": "{0} is downloading {1}",
@@ -86,6 +86,6 @@
     "Sync": "Sync",
     "User": "User",
     "System": "System",
-    "Application": "Application",
+    "Application": "Aplik\u00e1cia",
     "Plugin": "Plugin"
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/sv.json

@@ -27,7 +27,7 @@
     "Artists": "Artister",
     "Folders": "Mappar",
     "Songs": "L\u00e5tar",
-    "TvShows": "TV Shows",
+    "TvShows": "TV-serier",
     "Shows": "Serier",
     "Genres": "Genrer",
     "NameSeasonNumber": "S\u00e4song {0}",

+ 20 - 8
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Localization
 
             var localizationPath = LocalizationPath;
 
-			_fileSystem.CreateDirectory(localizationPath);
+            _fileSystem.CreateDirectory(localizationPath);
 
             var existingFiles = GetRatingsFiles(localizationPath)
                 .Select(Path.GetFileName)
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.Localization
                     }
                 }
             }
-            
+
             foreach (var file in GetRatingsFiles(localizationPath))
             {
                 LoadRatings(file);
@@ -128,12 +128,20 @@ namespace Emby.Server.Implementations.Localization
             return _textLocalizer.NormalizeFormKD(text);
         }
 
+        private CultureDto[] _cultures;
+
         /// <summary>
         /// Gets the cultures.
         /// </summary>
         /// <returns>IEnumerable{CultureDto}.</returns>
         public CultureDto[] GetCultures()
         {
+            var result = _cultures;
+            if (result != null)
+            {
+                return result;
+            }
+
             var type = GetType();
             var path = type.Namespace + ".iso6392.txt";
 
@@ -166,10 +174,14 @@ namespace Emby.Server.Implementations.Localization
                 }
             }
 
-            return list.Where(i => !string.IsNullOrWhiteSpace(i.Name) &&
-                !string.IsNullOrWhiteSpace(i.DisplayName) &&
-                !string.IsNullOrWhiteSpace(i.ThreeLetterISOLanguageName) &&
-                !string.IsNullOrWhiteSpace(i.TwoLetterISOLanguageName)).ToArray();
+            result = list.Where(i => !string.IsNullOrWhiteSpace(i.Name) &&
+               !string.IsNullOrWhiteSpace(i.DisplayName) &&
+               !string.IsNullOrWhiteSpace(i.ThreeLetterISOLanguageName) &&
+               !string.IsNullOrWhiteSpace(i.TwoLetterISOLanguageName)).ToArray();
+
+            _cultures = result;
+
+            return result;
         }
 
         /// <summary>
@@ -239,7 +251,7 @@ namespace Emby.Server.Implementations.Localization
         /// <returns>Dictionary{System.StringParentalRating}.</returns>
         private void LoadRatings(string file)
         {
-			var dict = _fileSystem.ReadAllLines(file).Select(i =>
+            var dict = _fileSystem.ReadAllLines(file).Select(i =>
             {
                 if (!string.IsNullOrWhiteSpace(i))
                 {
@@ -269,7 +281,7 @@ namespace Emby.Server.Implementations.Localization
             _allParentalRatings.TryAdd(countryCode, dict);
         }
 
-        private readonly string[] _unratedValues = {"n/a", "unrated", "not rated"};
+        private readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
 
         /// <summary>
         /// Gets the rating level.

+ 5 - 4
Emby.Server.Implementations/MediaEncoder/EncodingManager.cs

@@ -17,6 +17,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 
 namespace Emby.Server.Implementations.MediaEncoder
 {
@@ -89,7 +90,7 @@ namespace Emby.Server.Implementations.MediaEncoder
         /// </summary>
         private static readonly long FirstChapterTicks = TimeSpan.FromSeconds(15).Ticks;
 
-        public async Task<bool> RefreshChapterImages(Video video, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
+        public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
         {
             if (!IsEligibleForChapterImageExtraction(video))
             {
@@ -101,7 +102,7 @@ namespace Emby.Server.Implementations.MediaEncoder
 
             var runtimeTicks = video.RunTimeTicks ?? 0;
 
-            var currentImages = GetSavedChapterImages(video);
+            var currentImages = GetSavedChapterImages(video, directoryService);
 
             foreach (var chapter in chapters)
             {
@@ -194,13 +195,13 @@ namespace Emby.Server.Implementations.MediaEncoder
             return Path.Combine(GetChapterImagesPath(video), filename);
         }
 
-        private List<string> GetSavedChapterImages(Video video)
+        private List<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
         {
             var path = GetChapterImagesPath(video);
 
             try
             {
-                return _fileSystem.GetFilePaths(path)
+                return directoryService.GetFilePaths(path)
                     .ToList();
             }
             catch (IOException)

+ 4 - 1
Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs

@@ -16,6 +16,7 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Extensions;
+using MediaBrowser.Controller.Providers;
 
 namespace Emby.Server.Implementations.ScheduledTasks
 {
@@ -120,6 +121,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
                 previouslyFailedImages = new List<string>();
             }
 
+            var directoryService = new DirectoryService(_fileSystem);
+
             foreach (var video in videos)
             {
                 cancellationToken.ThrowIfCancellationRequested();
@@ -132,7 +135,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
                 {
                     var chapters = _itemRepo.GetChapters(video.Id);
 
-                    var success = await _encodingManager.RefreshChapterImages(video, chapters, extract, true, CancellationToken.None);
+                    var success = await _encodingManager.RefreshChapterImages(video, directoryService, chapters, extract, true, CancellationToken.None);
 
                     if (!success)
                     {

+ 5 - 0
MediaBrowser.Controller/Entities/Audio/Audio.cs

@@ -51,6 +51,11 @@ namespace MediaBrowser.Controller.Entities.Audio
             return 1;
         }
 
+        public bool SupportsMediaSourceSelection()
+        {
+            return false;
+        }
+
         [IgnoreDataMember]
         public override bool SupportsPlayedStatus
         {

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

@@ -2040,7 +2040,7 @@ namespace MediaBrowser.Controller.Entities
                 .Where(i => i.IsLocalFile)
                 .Select(i => FileSystem.GetDirectoryName(i.Path))
                 .Distinct(StringComparer.OrdinalIgnoreCase)
-                .SelectMany(i => FileSystem.GetFilePaths(i))
+                .SelectMany(i => directoryService.GetFilePaths(i))
                 .ToList();
 
             var deletedImages = ImageInfos

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

@@ -13,5 +13,6 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>Task{IEnumerable{MediaSourceInfo}}.</returns>
         List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution);
         List<MediaStream> GetMediaStreams();
+        bool SupportsMediaSourceSelection();
     }
 }

+ 5 - 0
MediaBrowser.Controller/Entities/Video.cs

@@ -109,6 +109,11 @@ namespace MediaBrowser.Controller.Entities
             get { return true; }
         }
 
+        public bool SupportsMediaSourceSelection()
+        {
+            return SourceType == SourceType.Library;
+        }
+
         /// <summary>
         /// Gets or sets the timestamp.
         /// </summary>

+ 5 - 0
MediaBrowser.Controller/LiveTv/LiveTvChannel.cs

@@ -24,6 +24,11 @@ namespace MediaBrowser.Controller.LiveTv
             return list;
         }
 
+        public bool SupportsMediaSourceSelection()
+        {
+            return false;
+        }
+
         public override UnratedItem GetBlockUnratedType()
         {
             return UnratedItem.LiveTvChannel;

+ 9 - 7
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -1325,6 +1325,8 @@ namespace MediaBrowser.Controller.MediaEncoding
             if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue)
             {
                 videoSizeParam = string.Format("scale={0}:{1}", state.VideoStream.Width.Value.ToString(_usCulture), state.VideoStream.Height.Value.ToString(_usCulture));
+
+                videoSizeParam += ":force_original_aspect_ratio=decrease";
             }
 
             var mapPrefix = state.SubtitleStream.IsExternal ?
@@ -1335,7 +1337,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 ? 0
                 : state.SubtitleStream.Index;
 
-            return string.Format(" -filter_complex \"[{0}:{1}]{4}[sub] ; [0:{2}] [sub] overlay{3}\"",
+            return string.Format(" -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\"",
                 mapPrefix.ToString(_usCulture),
                 subtitleStreamIndex.ToString(_usCulture),
                 state.VideoStream.Index.ToString(_usCulture),
@@ -2094,6 +2096,12 @@ namespace MediaBrowser.Controller.MediaEncoding
                     args += " -avoid_negative_ts disabled -start_at_zero";
                 }
 
+                // This is for internal graphical subs
+                if (hasGraphicalSubs)
+                {
+                    args += GetGraphicalSubtitleParam(state, encodingOptions, videoCodec);
+                }
+
                 var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultH264Preset);
 
                 if (!string.IsNullOrEmpty(qualityParam))
@@ -2101,12 +2109,6 @@ namespace MediaBrowser.Controller.MediaEncoding
                     args += " " + qualityParam.Trim();
                 }
 
-                // This is for internal graphical subs
-                if (hasGraphicalSubs)
-                {
-                    args += GetGraphicalSubtitleParam(state, encodingOptions, videoCodec);
-                }
-
                 if (!state.RunTimeTicks.HasValue)
                 {
                     args += " -flags -global_header";

+ 2 - 1
MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs

@@ -3,6 +3,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.Providers;
 
 namespace MediaBrowser.Controller.MediaEncoding
 {
@@ -11,6 +12,6 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <summary>
         /// Refreshes the chapter images.
         /// </summary>
-        Task<bool> RefreshChapterImages(Video video, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
+        Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
     }
 }

+ 28 - 24
MediaBrowser.Controller/Providers/DirectoryService.cs

@@ -14,11 +14,11 @@ namespace MediaBrowser.Controller.Providers
         private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
 
-        private readonly ConcurrentDictionary<string, FileSystemMetadata[]> _cache =
-            new ConcurrentDictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase);
+        private readonly Dictionary<string, FileSystemMetadata[]> _cache = new Dictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase);
 
-        private readonly ConcurrentDictionary<string, FileSystemMetadata> _fileCache =
-        new ConcurrentDictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+        private readonly Dictionary<string, FileSystemMetadata> _fileCache = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+
+        private readonly Dictionary<string, List<string>> _filePathCache = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
 
         public DirectoryService(ILogger logger, IFileSystem fileSystem)
         {
@@ -32,11 +32,6 @@ namespace MediaBrowser.Controller.Providers
         }
 
         public FileSystemMetadata[] GetFileSystemEntries(string path)
-        {
-            return GetFileSystemEntries(path, false);
-        }
-
-        private FileSystemMetadata[] GetFileSystemEntries(string path, bool clearCache)
         {
             if (string.IsNullOrWhiteSpace(path))
             {
@@ -45,13 +40,6 @@ namespace MediaBrowser.Controller.Providers
 
             FileSystemMetadata[] entries;
 
-            if (clearCache)
-            {
-                FileSystemMetadata[] removed;
-
-                _cache.TryRemove(path, out removed);
-            }
-
             if (!_cache.TryGetValue(path, out entries))
             {
                 //_logger.Debug("Getting files for " + path);
@@ -66,21 +54,17 @@ namespace MediaBrowser.Controller.Providers
                     entries = new FileSystemMetadata[] { };
                 }
 
-                _cache.TryAdd(path, entries);
+                //_cache.TryAdd(path, entries);
+                _cache[path] = entries;
             }
 
             return entries;
         }
 
         public List<FileSystemMetadata> GetFiles(string path)
-        {
-            return GetFiles(path, false);
-        }
-
-        public List<FileSystemMetadata> GetFiles(string path, bool clearCache)
         {
             var list = new List<FileSystemMetadata>();
-            var items = GetFileSystemEntries(path, clearCache);
+            var items = GetFileSystemEntries(path);
             foreach (var item in items)
             {
                 if (!item.IsDirectory)
@@ -100,7 +84,8 @@ namespace MediaBrowser.Controller.Providers
 
                 if (file != null && file.Exists)
                 {
-                    _fileCache.TryAdd(path, file);
+                    //_fileCache.TryAdd(path, file);
+                    _fileCache[path] = file;
                 }
                 else
                 {
@@ -111,5 +96,24 @@ namespace MediaBrowser.Controller.Providers
             return file;
             //return _fileSystem.GetFileInfo(path);
         }
+
+        public List<string> GetFilePaths(string path)
+        {
+            return GetFilePaths(path, false);
+        }
+
+        public List<string> GetFilePaths(string path, bool clearCache)
+        {
+            List<string> result;
+            if (clearCache || !_filePathCache.TryGetValue(path, out result))
+            {
+                result = _fileSystem.GetFilePaths(path).ToList();
+
+                _filePathCache[path] = result;
+            }
+
+            return result;
+        }
+
     }
 }

+ 3 - 0
MediaBrowser.Controller/Providers/IDirectoryService.cs

@@ -8,5 +8,8 @@ namespace MediaBrowser.Controller.Providers
         FileSystemMetadata[] GetFileSystemEntries(string path);
         List<FileSystemMetadata> GetFiles(string path);
         FileSystemMetadata GetFile(string path);
+
+        List<string> GetFilePaths(string path);
+        List<string> GetFilePaths(string path, bool clearCache);
     }
 }

+ 2 - 1
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -75,7 +75,8 @@ namespace MediaBrowser.Model.Dto
         public bool? CanDownload { get; set; }
 
         public bool? HasSubtitles { get; set; }
-        
+        public bool? SupportsMediaSourceSelection { get; set; }
+
         public string PreferredMetadataLanguage { get; set; }
         public string PreferredMetadataCountryCode { get; set; }
 

+ 2 - 0
MediaBrowser.Model/Providers/SubtitleOptions.cs

@@ -13,6 +13,7 @@ namespace MediaBrowser.Model.Providers
         public bool IsOpenSubtitleVipAccount { get; set; }
 
         public bool RequirePerfectMatch { get; set; }
+        public bool SaveSubtitlesInMediaFolders { get; set; }
 
         public SubtitleOptions()
         {
@@ -20,6 +21,7 @@ namespace MediaBrowser.Model.Providers
 
             SkipIfAudioTrackMatches = true;
             RequirePerfectMatch = true;
+            SaveSubtitlesInMediaFolders = true;
         }
     }
 }

+ 2 - 1
MediaBrowser.Model/Session/GeneralCommandType.cs

@@ -39,6 +39,7 @@
         ChannelDown = 31,
         SetMaxStreamingBitrate = 31,
         Guide = 32,
-        ToggleStats = 33
+        ToggleStats = 33,
+        PlayMediaSource = 34
     }
 }

+ 4 - 2
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -27,6 +27,7 @@ namespace MediaBrowser.Providers.Manager
         protected readonly IFileSystem FileSystem;
         protected readonly IUserDataManager UserDataManager;
         protected readonly ILibraryManager LibraryManager;
+        private readonly SubtitleResolver _subtitleResolver;
 
         protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager)
         {
@@ -36,6 +37,8 @@ namespace MediaBrowser.Providers.Manager
             FileSystem = fileSystem;
             UserDataManager = userDataManager;
             LibraryManager = libraryManager;
+
+            _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager, fileSystem);
         }
 
         public async Task<ItemUpdateType> RefreshMetadata(IHasMetadata item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
@@ -76,8 +79,7 @@ namespace MediaBrowser.Providers.Manager
                         if (video != null && !video.IsPlaceHolder)
                         {
                             requiresRefresh = !video.SubtitleFiles
-                                .SequenceEqual(SubtitleResolver.GetSubtitleFiles(video, refreshOptions.DirectoryService, FileSystem, false)
-                                .OrderBy(i => i), StringComparer.OrdinalIgnoreCase);
+                                .SequenceEqual(_subtitleResolver.GetExternalSubtitleFiles(video, refreshOptions.DirectoryService, false), StringComparer.Ordinal);
                         }
                     }
                 }

+ 3 - 3
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -229,7 +229,7 @@ namespace MediaBrowser.Providers.MediaInfo
                     extractDuringScan = libraryOptions.ExtractChapterImagesDuringLibraryScan;
                 }
 
-                await _encodingManager.RefreshChapterImages(video, chapters, extractDuringScan, false, cancellationToken).ConfigureAwait(false);
+                await _encodingManager.RefreshChapterImages(video, options.DirectoryService, chapters, extractDuringScan, false, cancellationToken).ConfigureAwait(false);
 
                 _chapterManager.SaveChapters(video.Id.ToString(), chapters);
             }
@@ -472,7 +472,7 @@ namespace MediaBrowser.Providers.MediaInfo
             var subtitleResolver = new SubtitleResolver(_localization, _fileSystem);
 
             var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1);
-            var externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, false).ToList();
+            var externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, false);
 
             var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default ||
                                             options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh;
@@ -497,7 +497,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 // Rescan
                 if (downloadedLanguages.Count > 0)
                 {
-                    externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, true).ToList();
+                    externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, true);
                 }
             }
 

+ 66 - 49
MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs

@@ -16,29 +16,91 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly ILocalizationManager _localization;
         private readonly IFileSystem _fileSystem;
 
+        private string[] SubtitleExtensions = new[]
+        {
+            ".srt",
+            ".ssa",
+            ".ass",
+            ".sub",
+            ".smi",
+            ".sami",
+            ".vtt"
+        };
+
         public SubtitleResolver(ILocalizationManager localization, IFileSystem fileSystem)
         {
             _localization = localization;
             _fileSystem = fileSystem;
         }
 
-        public IEnumerable<MediaStream> GetExternalSubtitleStreams(Video video,
+        public List<MediaStream> GetExternalSubtitleStreams(Video video,
           int startIndex,
           IDirectoryService directoryService,
           bool clearCache)
         {
-            var files = GetSubtitleFiles(video, directoryService, _fileSystem, clearCache);
-
             var streams = new List<MediaStream>();
 
-            var videoFileNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(video.Path);
+            GetExternalSubtitleStreams(streams, video.ContainingFolderPath, video.Path, startIndex, directoryService, clearCache);
+
+            startIndex += streams.Count;
+
+            try
+            {
+                GetExternalSubtitleStreams(streams, video.GetInternalMetadataPath(), video.Path, startIndex, directoryService, clearCache);
+            }
+            catch (IOException)
+            {
+
+            }
+
+            return streams;
+        }
+
+        public List<string> GetExternalSubtitleFiles(Video video,
+          IDirectoryService directoryService,
+          bool clearCache)
+        {
+            var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache);
+
+            var list = new List<string>();
+
+            foreach (var stream in streams)
+            {
+                list.Add(stream.Path);
+            }
+
+            return list;
+        }
+
+        private void GetExternalSubtitleStreams(List<MediaStream> streams, string folder,
+            string videoPath,
+            int startIndex,
+            IDirectoryService directoryService,
+            bool clearCache)
+        {
+            var videoFileNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(videoPath);
             videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoFileNameWithoutExtension);
 
+            var files = directoryService.GetFilePaths(folder, clearCache);
+
             foreach (var fullName in files)
             {
+                var extension = Path.GetExtension(fullName);
+
+                if (!SubtitleExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+                {
+                    continue;
+                }
+
                 var fileNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(fullName);
                 fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fileNameWithoutExtension);
 
+                if (!string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase) &&
+                    !fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase))
+                {
+                    continue;
+                }
+
                 var codec = Path.GetExtension(fullName).ToLower().TrimStart('.');
 
                 if (string.Equals(codec, "txt", StringComparison.OrdinalIgnoreCase))
@@ -98,8 +160,6 @@ namespace MediaBrowser.Providers.MediaInfo
                     });
                 }
             }
-
-            return streams;
         }
 
         private string NormalizeFilenameForSubtitleComparison(string filename)
@@ -115,48 +175,5 @@ namespace MediaBrowser.Providers.MediaInfo
 
             return filename;
         }
-
-        private static IEnumerable<string> SubtitleExtensions
-        {
-            get
-            {
-                return new[] { ".srt", ".ssa", ".ass", ".sub", ".smi", ".sami", ".vtt" };
-            }
-        }
-
-        public static IEnumerable<string> GetSubtitleFiles(Video video, IDirectoryService directoryService, IFileSystem fileSystem, bool clearCache)
-        {
-            var containingPath = video.ContainingFolderPath;
-
-            if (string.IsNullOrEmpty(containingPath))
-            {
-                throw new ArgumentException(string.Format("Cannot search for items that don't have a path: {0} {1}", video.Name, video.Id));
-            }
-
-            var files = fileSystem.GetFilePaths(containingPath, clearCache);
-
-            var videoFileNameWithoutExtension = fileSystem.GetFileNameWithoutExtension(video.Path);
-
-            return files.Where(i =>
-            {
-                var extension = Path.GetExtension(i);
-
-                if (SubtitleExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
-                {
-                    var fileNameWithoutExtension = fileSystem.GetFileNameWithoutExtension(i);
-
-                    if (string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
-                    {
-                        return true;
-                    }
-                    if (fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase))
-                    {
-                        return true;
-                    }
-                }
-
-                return false;
-            });
-        }
     }
 }

+ 66 - 31
MediaBrowser.Providers/Subtitles/SubtitleManager.cs

@@ -16,8 +16,8 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-
-using MediaBrowser.Controller.IO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.IO;
 
 namespace MediaBrowser.Providers.Subtitles
@@ -30,17 +30,19 @@ namespace MediaBrowser.Providers.Subtitles
         private readonly ILibraryMonitor _monitor;
         private readonly ILibraryManager _libraryManager;
         private readonly IMediaSourceManager _mediaSourceManager;
+        private readonly IServerConfigurationManager _config;
 
         public event EventHandler<SubtitleDownloadEventArgs> SubtitlesDownloaded;
         public event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure;
 
-        public SubtitleManager(ILogger logger, IFileSystem fileSystem, ILibraryMonitor monitor, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
+        public SubtitleManager(ILogger logger, IFileSystem fileSystem, ILibraryMonitor monitor, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IServerConfigurationManager config)
         {
             _logger = logger;
             _fileSystem = fileSystem;
             _monitor = monitor;
             _libraryManager = libraryManager;
             _mediaSourceManager = mediaSourceManager;
+            _config = config;
         }
 
         public void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders)
@@ -102,6 +104,11 @@ namespace MediaBrowser.Providers.Subtitles
             return results.SelectMany(i => i).ToArray();
         }
 
+        private SubtitleOptions GetOptions()
+        {
+            return _config.GetConfiguration<SubtitleOptions>("subtitles");
+        }
+
         public async Task DownloadSubtitles(Video video,
             string subtitleId,
             CancellationToken cancellationToken)
@@ -109,49 +116,37 @@ namespace MediaBrowser.Providers.Subtitles
             var parts = subtitleId.Split(new[] { '_' }, 2);
             var provider = GetProvider(parts.First());
 
+            var saveInMediaFolder = GetOptions().SaveSubtitlesInMediaFolders && video.SupportsLocalMetadata;
+
             try
             {
                 var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false);
 
                 using (var stream = response.Stream)
                 {
-                    var savePath = Path.Combine(_fileSystem.GetDirectoryName(video.Path),
-                        _fileSystem.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLower());
-
-                    if (response.IsForced)
+                    using (var memoryStream = new MemoryStream())
                     {
-                        savePath += ".forced";
-                    }
-
-                    savePath += "." + response.Format.ToLower();
+                        await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
+                        memoryStream.Position = 0;
 
-                    _logger.Info("Saving subtitles to {0}", savePath);
+                        var savePaths = new List<string>();
+                        var saveFileName = _fileSystem.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLower();
 
-                    _monitor.ReportFileSystemChangeBeginning(savePath);
+                        if (response.IsForced)
+                        {
+                            saveFileName += ".forced";
+                        }
 
-                    try
-                    {
-                        //var isText = MediaStream.IsTextFormat(response.Format);
+                        saveFileName += "." + response.Format.ToLower();
 
-                        using (var fs = _fileSystem.GetFileStream(savePath, FileOpenMode.Create, FileAccessMode.Write,
-                                FileShareMode.Read, true))
+                        if (saveInMediaFolder)
                         {
-                            await stream.CopyToAsync(fs).ConfigureAwait(false);
+                            savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName));
                         }
 
-                        EventHelper.FireEventIfNotNull(SubtitlesDownloaded, this, new SubtitleDownloadEventArgs
-                        {
-                            Item = video,
-                            Format = response.Format,
-                            Language = response.Language,
-                            IsForced = response.IsForced,
-                            Provider = provider.Name
+                        savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
 
-                        }, _logger);
-                    }
-                    finally
-                    {
-                        _monitor.ReportFileSystemChangeComplete(savePath, false);
+                        await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
                     }
                 }
             }
@@ -173,6 +168,46 @@ namespace MediaBrowser.Providers.Subtitles
             }
         }
 
+        private async Task TrySaveToFiles(Stream stream, List<string> savePaths)
+        {
+            Exception exceptionToThrow = null;
+
+            foreach (var savePath in savePaths)
+            {
+                _logger.Info("Saving subtitles to {0}", savePath);
+
+                _monitor.ReportFileSystemChangeBeginning(savePath);
+
+                try
+                {
+                    using (var fs = _fileSystem.GetFileStream(savePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
+                    {
+                        await stream.CopyToAsync(fs).ConfigureAwait(false);
+                    }
+
+                    return;
+                }
+                catch (Exception ex)
+                {
+                    if (exceptionToThrow == null)
+                    {
+                        exceptionToThrow = ex;
+                    }
+                }
+                finally
+                {
+                    _monitor.ReportFileSystemChangeComplete(savePath, false);
+                }
+
+                stream.Position = 0;
+            }
+
+            if (exceptionToThrow != null)
+            {
+                throw exceptionToThrow;
+            }
+        }
+
         public Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, string language, bool? isPerfectMatch, CancellationToken cancellationToken)
         {
             if (video.LocationType != LocationType.FileSystem ||

+ 1 - 1
SharedVersion.cs

@@ -1,3 +1,3 @@
 using System.Reflection;
 
-[assembly: AssemblyVersion("3.2.36.1")]
+[assembly: AssemblyVersion("3.2.36.2")]

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