Explorar o código

Merge pull request #73 from jellyfin/master

Updating from upstream
BaronGreenback %!s(int64=4) %!d(string=hai) anos
pai
achega
4c6b60d69d

+ 1 - 0
CONTRIBUTORS.md

@@ -198,3 +198,4 @@
  - [tikuf](https://github.com/tikuf/)
  - [Tim Hobbs](https://github.com/timhobbs)
  - [SvenVandenbrande](https://github.com/SvenVandenbrande)
+ - [olsh](https://github.com/olsh)

+ 1 - 1
Emby.Drawing/ImageProcessor.cs

@@ -455,7 +455,7 @@ namespace Emby.Drawing
                 throw new ArgumentException("Path can't be empty.", nameof(path));
             }
 
-            if (path.IsEmpty)
+            if (filename.IsEmpty)
             {
                 throw new ArgumentException("Filename can't be empty.", nameof(filename));
             }

+ 2 - 15
Emby.Naming/AudioBook/AudioBookFilePathParser.cs

@@ -50,27 +50,14 @@ namespace Emby.Naming.AudioBook
                         {
                             if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
                             {
-                                result.ChapterNumber = intValue;
+                                result.PartNumber = intValue;
                             }
                         }
                     }
                 }
             }
 
-            /*var matches = _iRegexProvider.GetRegex("\\d+", RegexOptions.IgnoreCase).Matches(fileName);
-            if (matches.Count > 0)
-            {
-                if (!result.ChapterNumber.HasValue)
-                {
-                    result.ChapterNumber = int.Parse(matches[0].Groups[0].Value);
-                }
-
-                if (matches.Count > 1)
-                {
-                    result.PartNumber = int.Parse(matches[matches.Count - 1].Groups[0].Value);
-                }
-            }*/
-            result.Success = result.PartNumber.HasValue || result.ChapterNumber.HasValue;
+            result.Success = result.ChapterNumber.HasValue || result.PartNumber.HasValue;
 
             return result;
         }

+ 1 - 1
Emby.Naming/AudioBook/AudioBookResolver.cs

@@ -55,8 +55,8 @@ namespace Emby.Naming.AudioBook
             {
                 Path = path,
                 Container = container,
-                PartNumber = parsingResult.PartNumber,
                 ChapterNumber = parsingResult.ChapterNumber,
+                PartNumber = parsingResult.PartNumber,
                 IsDirectory = isDirectory
             };
         }

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

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net;
@@ -37,6 +38,7 @@ using Emby.Server.Implementations.LiveTv;
 using Emby.Server.Implementations.Localization;
 using Emby.Server.Implementations.Net;
 using Emby.Server.Implementations.Playlists;
+using Emby.Server.Implementations.Plugins;
 using Emby.Server.Implementations.QuickConnect;
 using Emby.Server.Implementations.ScheduledTasks;
 using Emby.Server.Implementations.Security;
@@ -119,6 +121,7 @@ namespace Emby.Server.Implementations
         private readonly IFileSystem _fileSystemManager;
         private readonly INetworkManager _networkManager;
         private readonly IXmlSerializer _xmlSerializer;
+        private readonly IJsonSerializer _jsonSerializer;
         private readonly IStartupOptions _startupOptions;
 
         private IMediaEncoder _mediaEncoder;
@@ -255,6 +258,8 @@ namespace Emby.Server.Implementations
             IServiceCollection serviceCollection)
         {
             _xmlSerializer = new MyXmlSerializer();
+            _jsonSerializer = new JsonSerializer();            
+            
             ServiceCollection = serviceCollection;
 
             _networkManager = networkManager;
@@ -1021,6 +1026,108 @@ namespace Emby.Server.Implementations
 
         protected abstract void RestartInternal();
 
+        /// <summary>
+        /// Comparison function used in <see cref="GetPlugins" />.
+        /// </summary>
+        /// <param name="a">Item to compare.</param>
+        /// <param name="b">Item to compare with.</param>
+        /// <returns>Boolean result of the operation.</returns>
+        private static int VersionCompare(
+            (Version PluginVersion, string Name, string Path) a,
+            (Version PluginVersion, string Name, string Path) b)
+        {
+            int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
+
+            if (compare == 0)
+            {
+                return a.PluginVersion.CompareTo(b.PluginVersion);
+            }
+
+            return compare;
+        }
+
+        /// <summary>
+        /// Returns a list of plugins to install.
+        /// </summary>
+        /// <param name="path">Path to check.</param>
+        /// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
+        /// <returns>Enumerable list of dlls to load.</returns>
+        private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
+        {
+            var dllList = new List<string>();
+            var versions = new List<(Version PluginVersion, string Name, string Path)>();
+            var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
+            string metafile;
+
+            foreach (var dir in directories)
+            {
+                try
+                {
+                    metafile = Path.Combine(dir, "meta.json");
+                    if (File.Exists(metafile))
+                    {
+                        var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
+
+                        if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
+                        {
+                            targetAbi = new Version(0, 0, 0, 1);
+                        }
+
+                        if (!Version.TryParse(manifest.Version, out var version))
+                        {
+                            version = new Version(0, 0, 0, 1);
+                        }
+
+                        if (ApplicationVersion >= targetAbi)
+                        {
+                            // Only load Plugins if the plugin is built for this version or below.
+                            versions.Add((version, manifest.Name, dir));
+                        }
+                    }
+                    else
+                    {
+                        metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
+                        // Add it under the path name and version 0.0.0.1.
+                        versions.Add((new Version(0, 0, 0, 1), metafile, dir));
+                    }
+                }
+                catch
+                {
+                    continue;
+                }
+            }
+
+            string lastName = string.Empty;
+            versions.Sort(VersionCompare);
+            // Traverse backwards through the list.
+            // The first item will be the latest version.
+            for (int x = versions.Count - 1; x >= 0; x--)
+            {
+                if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
+                {
+                    dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
+                    lastName = versions[x].Name;
+                    continue;
+                }
+
+                if (!string.IsNullOrEmpty(lastName) && cleanup)
+                {
+                    // Attempt a cleanup of old folders.
+                    try
+                    {
+                        Logger.LogDebug("Deleting {Path}", versions[x].Path);
+                        Directory.Delete(versions[x].Path, true);
+                    }
+                    catch (Exception e)
+                    {
+                        Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
+                    }
+                }
+            }
+
+            return dllList;
+        }
+
         /// <summary>
         /// Gets the composable part assemblies.
         /// </summary>
@@ -1029,7 +1136,7 @@ namespace Emby.Server.Implementations
         {
             if (Directory.Exists(ApplicationPaths.PluginsPath))
             {
-                foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
+                foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
                 {
                     Assembly plugAss;
                     try

+ 117 - 0
Emby.Server.Implementations/Localization/Core/sq.json

@@ -0,0 +1,117 @@
+{
+    "MessageApplicationUpdatedTo": "Serveri Jellyfin u përditesua në versionin {0}",
+    "Inherit": "Trashgimi",
+    "TaskDownloadMissingSubtitlesDescription": "Kërkon në internet për titra që mungojnë bazuar tek konfigurimi i metadata-ve.",
+    "TaskDownloadMissingSubtitles": "Shkarko titra që mungojnë",
+    "TaskRefreshChannelsDescription": "Rifreskon informacionin e kanaleve të internetit.",
+    "TaskRefreshChannels": "Rifresko Kanalet",
+    "TaskCleanTranscodeDescription": "Fshin skedarët e transkodimit që janë më të vjetër se një ditë.",
+    "TaskCleanTranscode": "Fshi dosjen e transkodimit",
+    "TaskUpdatePluginsDescription": "Shkarkon dhe instalon përditësimi për plugin që janë konfiguruar të përditësohen automatikisht.",
+    "TaskUpdatePlugins": "Përditëso Plugin",
+    "TaskRefreshPeopleDescription": "Përditëson metadata të aktorëve dhe regjizorëve në librarinë tuaj.",
+    "TaskRefreshPeople": "Rifresko aktorët",
+    "TaskCleanLogsDescription": "Fshin skëdarët log që janë më të vjetër se {0} ditë.",
+    "TaskCleanLogs": "Fshi dosjen Log",
+    "TaskRefreshLibraryDescription": "Skanon librarinë media për skedarë të rinj dhe rifreskon metadata.",
+    "TaskRefreshLibrary": "Skano librarinë media",
+    "TaskRefreshChapterImagesDescription": "Krijon imazh për videot që kanë kapituj.",
+    "TaskRefreshChapterImages": "Ekstrakto Imazhet e Kapitullit",
+    "TaskCleanCacheDescription": "Fshi skedarët e cache-s që nuk i duhen më sistemit.",
+    "TaskCleanCache": "Pastro memorjen cache",
+    "TasksChannelsCategory": "Kanalet nga interneti",
+    "TasksApplicationCategory": "Aplikacioni",
+    "TasksLibraryCategory": "Libraria",
+    "TasksMaintenanceCategory": "Mirëmbajtje",
+    "VersionNumber": "Versioni {0}",
+    "ValueSpecialEpisodeName": "Speciale - {0}",
+    "ValueHasBeenAddedToLibrary": "{0} u shtua tek libraria juaj",
+    "UserStoppedPlayingItemWithValues": "{0} mbaroi së shikuari {1} tek {2}",
+    "UserStartedPlayingItemWithValues": "{0} po shikon {1} tek {2}",
+    "UserPolicyUpdatedWithName": "Politika e përdoruesit u përditësua për {0}",
+    "UserPasswordChangedWithName": "Fjalëkalimi u ndryshua për përdoruesin {0}",
+    "UserOnlineFromDevice": "{0} është në linjë nga {1}",
+    "UserOfflineFromDevice": "{0} u shkëput nga {1}",
+    "UserLockedOutWithName": "Përdoruesi {0} u përjashtua",
+    "UserDownloadingItemWithValues": "{0} po shkarkon {1}",
+    "UserDeletedWithName": "Përdoruesi {0} u fshi",
+    "UserCreatedWithName": "Përdoruesi {0} u krijua",
+    "User": "Përdoruesi",
+    "TvShows": "Seriale TV",
+    "System": "Sistemi",
+    "Sync": "Sinkronizo",
+    "SubtitleDownloadFailureFromForItem": "Titrat deshtuan të shkarkohen nga {0} për {1}",
+    "StartupEmbyServerIsLoading": "Serveri Jellyfin po ngarkohet. Ju lutemi provoni përseri pas pak.",
+    "Songs": "Këngë",
+    "Shows": "Seriale",
+    "ServerNameNeedsToBeRestarted": "{0} duhet të ristartoj",
+    "ScheduledTaskStartedWithName": "{0} filloi",
+    "ScheduledTaskFailedWithName": "{0} dështoi",
+    "ProviderValue": "Ofruesi: {0}",
+    "PluginUpdatedWithName": "{0} u përditësua",
+    "PluginUninstalledWithName": "{0} u çinstalua",
+    "PluginInstalledWithName": "{0} u instalua",
+    "Plugin": "Plugin",
+    "Playlists": "Listat për luajtje",
+    "Photos": "Fotografitë",
+    "NotificationOptionVideoPlaybackStopped": "Luajtja e videos ndaloi",
+    "NotificationOptionVideoPlayback": "Luajtja e videos filloi",
+    "NotificationOptionUserLockedOut": "Përdoruesi u përjashtua",
+    "NotificationOptionTaskFailed": "Ushtrimi i planifikuar dështoi",
+    "NotificationOptionServerRestartRequired": "Kërkohet ristartim i serverit",
+    "NotificationOptionPluginUpdateInstalled": "Përditësimi i plugin u instalua",
+    "NotificationOptionPluginUninstalled": "Plugin u çinstalua",
+    "NotificationOptionPluginInstalled": "Plugin u instalua",
+    "NotificationOptionPluginError": "Plugin dështoi",
+    "NotificationOptionNewLibraryContent": "Një përmbajtje e re u shtua",
+    "NotificationOptionInstallationFailed": "Instalimi dështoi",
+    "NotificationOptionCameraImageUploaded": "Fotoja nga kamera u ngarkua",
+    "NotificationOptionAudioPlaybackStopped": "Luajtja e audios ndaloi",
+    "NotificationOptionAudioPlayback": "Luajtja e audios filloi",
+    "NotificationOptionApplicationUpdateInstalled": "Përditësimi i aplikacionit u instalua",
+    "NotificationOptionApplicationUpdateAvailable": "Një perditësim i aplikacionit është gati",
+    "NewVersionIsAvailable": "Një version i ri i Jellyfin është gati për tu shkarkuar.",
+    "NameSeasonUnknown": "Sezon i panjohur",
+    "NameSeasonNumber": "Sezoni {0}",
+    "NameInstallFailed": "Instalimi i {0} dështoi",
+    "MusicVideos": "Video muzikore",
+    "Music": "Muzikë",
+    "Movies": "Filma",
+    "MixedContent": "Përmbajtje e përzier",
+    "MessageServerConfigurationUpdated": "Konfigurimet e serverit u përditësuan",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Seksioni i konfigurimit të serverit {0} u përditësua",
+    "MessageApplicationUpdated": "Serveri Jellyfin u përditësua",
+    "Latest": "Të fundit",
+    "LabelRunningTimeValue": "Kohëzgjatja: {0}",
+    "LabelIpAddressValue": "Adresa IP: {0}",
+    "ItemRemovedWithName": "{0} u fshi nga libraria",
+    "ItemAddedWithName": "{0} u shtua tek libraria",
+    "HomeVideos": "Video personale",
+    "HeaderRecordingGroups": "Grupet e regjistrimit",
+    "HeaderNextUp": "Në vazhdim",
+    "HeaderLiveTV": "TV Live",
+    "HeaderFavoriteSongs": "Kënget e preferuara",
+    "HeaderFavoriteShows": "Serialet e preferuar",
+    "HeaderFavoriteEpisodes": "Episodet e preferuar",
+    "HeaderFavoriteArtists": "Artistët e preferuar",
+    "HeaderFavoriteAlbums": "Albumet e preferuar",
+    "HeaderContinueWatching": "Vazhdo të shikosh",
+    "HeaderCameraUploads": "Ngarkimet nga Kamera",
+    "HeaderAlbumArtists": "Artistët e albumeve",
+    "Genres": "Zhanre",
+    "Folders": "Dosje",
+    "Favorites": "Të preferuara",
+    "FailedLoginAttemptWithUserName": "Përpjekja për hyrje dështoi nga {0}",
+    "DeviceOnlineWithName": "{0} u lidh",
+    "DeviceOfflineWithName": "{0} u shkëput",
+    "Collections": "Koleksione",
+    "ChapterNameValue": "Kapituj",
+    "Channels": "Kanale",
+    "CameraImageUploadedFrom": "Një foto e re nga kamera u ngarkua nga {0}",
+    "Books": "Libra",
+    "AuthenticationSucceededWithUserName": "{0} u identifikua me sukses",
+    "Artists": "Artistë",
+    "Application": "Aplikacioni",
+    "AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
+    "Albums": "Albumet"
+}

+ 117 - 0
Emby.Server.Implementations/Localization/Core/vi.json

@@ -0,0 +1,117 @@
+{
+    "Collections": "Bộ Sưu Tập",
+    "Favorites": "Sở Thích",
+    "Folders": "Thư Mục",
+    "Genres": "Thể Loại",
+    "HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ",
+    "HeaderContinueWatching": "Tiếp Tục Xem Tiếp",
+    "HeaderLiveTV": "TV Trực Tiếp",
+    "Movies": "Phim",
+    "Photos": "Ảnh",
+    "Playlists": "Danh Sách Chơi",
+    "Shows": "Các Chương Trình",
+    "Songs": "Các Bài Hát",
+    "Sync": "Đồng Bộ",
+    "ValueSpecialEpisodeName": "Đặc Biệt - {0}",
+    "Albums": "Bộ Sưu Tập",
+    "Artists": "Các Nghệ Sĩ",
+    "TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình thông tin chi tiết.",
+    "TaskDownloadMissingSubtitles": "Tải xuống phụ đề bị thiếu",
+    "TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
+    "TaskRefreshChannels": "Làm Mới Kênh",
+    "TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.",
+    "TaskCleanTranscode": "Làm Sạch Thư Mục Chuyển Mã",
+    "TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.",
+    "TaskUpdatePlugins": "Cập Nhật Plugins",
+    "TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.",
+    "TaskRefreshPeople": "Làm mới Người dùng",
+    "TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.",
+    "TaskCleanLogs": "Làm sạch nhật ký",
+    "TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm các tệp mới và làm mới thông tin chi tiết.",
+    "TaskRefreshLibrary": "Quét Thư viện Phương tiện",
+    "TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho các video có chương.",
+    "TaskRefreshChapterImages": "Trích xuất hình ảnh chương",
+    "TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
+    "TaskCleanCache": "Làm Sạch Thư Mục Cache",
+    "TasksChannelsCategory": "Kênh Internet",
+    "TasksApplicationCategory": "Ứng Dụng",
+    "TasksLibraryCategory": "Thư Viện",
+    "TasksMaintenanceCategory": "Bảo Trì",
+    "VersionNumber": "Phiên Bản {0}",
+    "ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn",
+    "UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}",
+    "UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}",
+    "UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}",
+    "UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}",
+    "UserOnlineFromDevice": "{0} trực tuyến từ {1}",
+    "UserOfflineFromDevice": "{0} đã ngắt kết nối từ {1}",
+    "UserLockedOutWithName": "User {0} đã bị khóa",
+    "UserDownloadingItemWithValues": "{0} đang tải xuống {1}",
+    "UserDeletedWithName": "Người Dùng {0} đã được xóa",
+    "UserCreatedWithName": "Người Dùng {0} đã được tạo",
+    "User": "Người Dùng",
+    "TvShows": "Chương Trình TV",
+    "System": "Hệ Thống",
+    "SubtitleDownloadFailureFromForItem": "Không thể tải xuống phụ đề từ {0} cho {1}",
+    "StartupEmbyServerIsLoading": "Jellyfin Server đang tải. Vui lòng thử lại trong thời gian ngắn.",
+    "ServerNameNeedsToBeRestarted": "{0} cần được khởi động lại",
+    "ScheduledTaskStartedWithName": "{0} đã bắt đầu",
+    "ScheduledTaskFailedWithName": "{0} đã thất bại",
+    "ProviderValue": "Provider: {0}",
+    "PluginUpdatedWithName": "{0} đã cập nhật",
+    "PluginUninstalledWithName": "{0} đã được gỡ bỏ",
+    "PluginInstalledWithName": "{0} đã được cài đặt",
+    "Plugin": "Plugin",
+    "NotificationOptionVideoPlaybackStopped": "Phát lại video đã dừng",
+    "NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video",
+    "NotificationOptionUserLockedOut": "Người dùng bị khóa",
+    "NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch",
+    "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server",
+    "NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt",
+    "NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin",
+    "NotificationOptionPluginInstalled": "Đã cài đặt Plugin",
+    "NotificationOptionPluginError": "Thất bại Plugin",
+    "NotificationOptionNewLibraryContent": "Nội dung mới được thêm vào",
+    "NotificationOptionInstallationFailed": "Cài đặt thất bại",
+    "NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh",
+    "NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng",
+    "NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu",
+    "NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt",
+    "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có",
+    "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.",
+    "NameSeasonUnknown": "Không Rõ Mùa",
+    "NameSeasonNumber": "Mùa {0}",
+    "NameInstallFailed": "{0} cài đặt thất bại",
+    "MusicVideos": "Video Nhạc",
+    "Music": "Nhạc",
+    "MixedContent": "Nội dung hỗn hợp",
+    "MessageServerConfigurationUpdated": "Cấu hình máy chủ đã được cập nhật",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Phần cấu hình máy chủ {0} đã được cập nhật",
+    "MessageApplicationUpdatedTo": "Jellyfin Server đã được cập nhật lên {0}",
+    "MessageApplicationUpdated": "Jellyfin Server đã được cập nhật",
+    "Latest": "Gần Nhất",
+    "LabelRunningTimeValue": "Thời Gian Chạy: {0}",
+    "LabelIpAddressValue": "Địa Chỉ IP: {0}",
+    "ItemRemovedWithName": "{0} đã xóa khỏi thư viện",
+    "ItemAddedWithName": "{0} được thêm vào thư viện",
+    "Inherit": "Thừa hưởng",
+    "HomeVideos": "Video nhà",
+    "HeaderRecordingGroups": "Nhóm Ghi Video",
+    "HeaderNextUp": "Tiếp Theo",
+    "HeaderFavoriteSongs": "Bài Hát Yêu Thích",
+    "HeaderFavoriteShows": "Chương Trình Yêu Thích",
+    "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
+    "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
+    "HeaderFavoriteAlbums": "Album Ưa Thích",
+    "HeaderCameraUploads": "Máy Ảnh Tải Lên",
+    "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
+    "DeviceOnlineWithName": "{0} đã kết nối",
+    "DeviceOfflineWithName": "{0} đã ngắt kết nối",
+    "ChapterNameValue": "Chương {0}",
+    "Channels": "Các Kênh",
+    "CameraImageUploadedFrom": "Một hình ảnh máy ảnh mới đã được tải lên từ {0}",
+    "Books": "Các Quyển Sách",
+    "AuthenticationSucceededWithUserName": "{0} xác thực thành công",
+    "Application": "Ứng Dụng",
+    "AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}"
+}

+ 60 - 0
Emby.Server.Implementations/Plugins/PluginManifest.cs

@@ -0,0 +1,60 @@
+using System;
+
+namespace Emby.Server.Implementations.Plugins
+{
+    /// <summary>
+    /// Defines a Plugin manifest file.
+    /// </summary>
+    public class PluginManifest
+    {
+        /// <summary>
+        /// Gets or sets the category of the plugin.
+        /// </summary>
+        public string Category { get; set; }
+
+        /// <summary>
+        /// Gets or sets the changelog information.
+        /// </summary>
+        public string Changelog { get; set; }
+
+        /// <summary>
+        /// Gets or sets the description of the plugin.
+        /// </summary>
+        public string Description { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Global Unique Identifier for the plugin.
+        /// </summary>
+        public Guid Guid { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Name of the plugin.
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets an overview of the plugin.
+        /// </summary>
+        public string Overview { get; set; }
+
+        /// <summary>
+        /// Gets or sets the owner of the plugin.
+        /// </summary>
+        public string Owner { get; set; }
+
+        /// <summary>
+        /// Gets or sets the compatibility version for the plugin.
+        /// </summary>
+        public string TargetAbi { get; set; }
+
+        /// <summary>
+        /// Gets or sets the timestamp of the plugin.
+        /// </summary>
+        public DateTime Timestamp { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Version number of the plugin.
+        /// </summary>
+        public string Version { get; set; }
+    }
+}

+ 26 - 8
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -15,12 +15,14 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
+using MediaBrowser.Common.System;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Updates;
 using Microsoft.Extensions.Logging;
+using MediaBrowser.Model.System;
 
 namespace Emby.Server.Implementations.Updates
 {
@@ -377,11 +379,20 @@ namespace Emby.Server.Implementations.Updates
                 throw new InvalidDataException("The checksum of the received data doesn't match.");
             }
 
+            // Version folder as they cannot be overwritten in Windows.
+            targetDir += "_" + package.Version;
+
             if (Directory.Exists(targetDir))
             {
-                Directory.Delete(targetDir, true);
+                try
+                {
+                    Directory.Delete(targetDir, true);
+                }
+                catch
+                {
+                    // Ignore any exceptions.
+                }
             }
-
             stream.Position = 0;
             _zipClient.ExtractAllFromZip(stream, targetDir, true);
 
@@ -423,15 +434,22 @@ namespace Emby.Server.Implementations.Updates
                 path = file;
             }
 
-            if (isDirectory)
+            try
             {
-                _logger.LogInformation("Deleting plugin directory {0}", path);
-                Directory.Delete(path, true);
+                if (isDirectory)
+                {
+                    _logger.LogInformation("Deleting plugin directory {0}", path);
+                    Directory.Delete(path, true);
+                }
+                else
+                {
+                    _logger.LogInformation("Deleting plugin file {0}", path);
+                    _fileSystem.DeleteFile(path);
+                }
             }
-            else
+            catch
             {
-                _logger.LogInformation("Deleting plugin file {0}", path);
-                _fileSystem.DeleteFile(path);
+                // Ignore file errors.
             }
 
             var list = _config.Configuration.UninstalledPlugins.ToList();

+ 1 - 1
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -20,7 +20,7 @@
   <ItemGroup>
     <PackageReference Include="BlurHashSharp" Version="1.1.0" />
     <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.0" />
-    <PackageReference Include="SkiaSharp" Version="2.80.1" />
+    <PackageReference Include="SkiaSharp" Version="2.80.2" />
     <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.1" />
   </ItemGroup>
 

+ 1 - 0
MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs

@@ -141,6 +141,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
                     Name = episode.EpisodeName,
                     Overview = episode.Overview,
                     CommunityRating = (float?)episode.SiteRating,
+                    OfficialRating = episode.ContentRating,
                 }
             };
             result.ResetPeople();

+ 1 - 1
README.md

@@ -124,7 +124,7 @@ To run the project with Visual Studio Code you will first need to open the repos
 
 Second, you need to [install the recommended extensions for the workspace](https://code.visualstudio.com/docs/editor/extension-gallery#_recommended-extensions). Note that extension recommendations are classified as either "Workspace Recommendations" or "Other Recommendations", but only the "Workspace Recommendations" are required.
 
-After the required extensions are installed, you can can run the server by pressing `F5`.
+After the required extensions are installed, you can run the server by pressing `F5`.
 
 #### Running From The Command Line
 

+ 90 - 0
tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs

@@ -0,0 +1,90 @@
+using System.Linq;
+using Emby.Naming.AudioBook;
+using Emby.Naming.Common;
+using MediaBrowser.Model.IO;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.AudioBook
+{
+    public class AudioBookListResolverTests
+    {
+        private readonly NamingOptions _namingOptions = new NamingOptions();
+
+        [Fact]
+        public void TestStackAndExtras()
+        {
+            // No stacking here because there is no part/disc/etc
+            var files = new[]
+            {
+                "Harry Potter and the Deathly Hallows/Part 1.mp3",
+                "Harry Potter and the Deathly Hallows/Part 2.mp3",
+                "Harry Potter and the Deathly Hallows/book.nfo",
+
+                "Batman/Chapter 1.mp3",
+                "Batman/Chapter 2.mp3",
+                "Batman/Chapter 3.mp3",
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+            })).ToList();
+
+            Assert.Equal(2, result[0].Files.Count);
+            // Assert.Empty(result[0].Extras); FIXME: AudioBookListResolver should resolve extra files properly
+            Assert.Equal("Harry Potter and the Deathly Hallows", result[0].Name);
+
+            Assert.Equal(3, result[1].Files.Count);
+            Assert.Empty(result[1].Extras);
+            Assert.Equal("Batman", result[1].Name);
+        }
+
+        [Fact]
+        public void TestWithMetadata()
+        {
+            var files = new[]
+            {
+                "Harry Potter and the Deathly Hallows/Chapter 1.ogg",
+                "Harry Potter and the Deathly Hallows/Harry Potter and the Deathly Hallows.nfo"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+            }));
+
+            Assert.Single(result);
+        }
+
+        [Fact]
+        public void TestWithExtra()
+        {
+            var files = new[]
+            {
+                "Harry Potter and the Deathly Hallows/Chapter 1.mp3",
+                "Harry Potter and the Deathly Hallows/Harry Potter and the Deathly Hallows trailer.mp3"
+            };
+
+            var resolver = GetResolver();
+
+            var result = resolver.Resolve(files.Select(i => new FileSystemMetadata
+            {
+                IsDirectory = false,
+                FullName = i
+            })).ToList();
+
+            Assert.Single(result);
+        }
+
+        private AudioBookListResolver GetResolver()
+        {
+            return new AudioBookListResolver(_namingOptions);
+        }
+    }
+}

+ 57 - 0
tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs

@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using Emby.Naming.AudioBook;
+using Emby.Naming.Common;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.AudioBook
+{
+    public class AudioBookResolverTests
+    {
+        private readonly NamingOptions _namingOptions = new NamingOptions();
+
+        public static IEnumerable<object[]> GetResolveFileTestData()
+        {
+            yield return new object[]
+            {
+                new AudioBookFileInfo()
+                {
+                    Path = @"/server/AudioBooks/Larry Potter/Larry Potter.mp3",
+                    Container = "mp3",
+                }
+            };
+            yield return new object[]
+            {
+                new AudioBookFileInfo()
+                {
+                    Path = @"/server/AudioBooks/Berry Potter/Chapter 1 .ogg",
+                    Container = "ogg",
+                    ChapterNumber = 1
+                }
+            };
+            yield return new object[]
+            {
+                new AudioBookFileInfo()
+                {
+                    Path = @"/server/AudioBooks/Nerry Potter/Part 3 - Chapter 2.mp3",
+                    Container = "mp3",
+                    ChapterNumber = 2,
+                    PartNumber = 3
+                }
+            };
+        }
+
+        [Theory]
+        [MemberData(nameof(GetResolveFileTestData))]
+        public void ResolveFile_ValidFileName_Success(AudioBookFileInfo expectedResult)
+        {
+            var result = new AudioBookResolver(_namingOptions).Resolve(expectedResult.Path);
+
+            Assert.NotNull(result);
+            Assert.Equal(result.Path, expectedResult.Path);
+            Assert.Equal(result.Container, expectedResult.Container);
+            Assert.Equal(result.ChapterNumber, expectedResult.ChapterNumber);
+            Assert.Equal(result.PartNumber, expectedResult.PartNumber);
+            Assert.Equal(result.IsDirectory, expectedResult.IsDirectory);
+        }
+    }
+}