diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f0724b4129..f1fe65064b 100644 --- a/CONTRIBUTORS.md +++ b/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) diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index f585b90ca1..ed20292f6b 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/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)); } diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs index 3c874c62ca..eb9393b0bd 100644 --- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs +++ b/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; } diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs index 5466b46379..ed53bd04fa 100644 --- a/Emby.Naming/AudioBook/AudioBookResolver.cs +++ b/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 }; } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 642e2fdbe9..276d0fe30a 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/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(); + /// + /// Comparison function used in . + /// + /// Item to compare. + /// Item to compare with. + /// Boolean result of the operation. + 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; + } + + /// + /// Returns a list of plugins to install. + /// + /// Path to check. + /// True if an attempt should be made to delete old plugs. + /// Enumerable list of dlls to load. + private IEnumerable GetPlugins(string path, bool cleanup = true) + { + var dllList = new List(); + 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(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; + } + /// /// Gets the composable part assemblies. /// @@ -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 diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json new file mode 100644 index 0000000000..347ba5f970 --- /dev/null +++ b/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" +} diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json new file mode 100644 index 0000000000..bf4cfe75ca --- /dev/null +++ b/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}" +} diff --git a/Emby.Server.Implementations/Plugins/PluginManifest.cs b/Emby.Server.Implementations/Plugins/PluginManifest.cs new file mode 100644 index 0000000000..33762791bc --- /dev/null +++ b/Emby.Server.Implementations/Plugins/PluginManifest.cs @@ -0,0 +1,60 @@ +using System; + +namespace Emby.Server.Implementations.Plugins +{ + /// + /// Defines a Plugin manifest file. + /// + public class PluginManifest + { + /// + /// Gets or sets the category of the plugin. + /// + public string Category { get; set; } + + /// + /// Gets or sets the changelog information. + /// + public string Changelog { get; set; } + + /// + /// Gets or sets the description of the plugin. + /// + public string Description { get; set; } + + /// + /// Gets or sets the Global Unique Identifier for the plugin. + /// + public Guid Guid { get; set; } + + /// + /// Gets or sets the Name of the plugin. + /// + public string Name { get; set; } + + /// + /// Gets or sets an overview of the plugin. + /// + public string Overview { get; set; } + + /// + /// Gets or sets the owner of the plugin. + /// + public string Owner { get; set; } + + /// + /// Gets or sets the compatibility version for the plugin. + /// + public string TargetAbi { get; set; } + + /// + /// Gets or sets the timestamp of the plugin. + /// + public DateTime Timestamp { get; set; } + + /// + /// Gets or sets the Version number of the plugin. + /// + public string Version { get; set; } + } +} diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index e19158f8e9..8e24bf55ce 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/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(); diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index c71c76f08f..6b378034a5 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -20,7 +20,7 @@ - + diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs index c088d8cec1..5fa8a3e1ca 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs +++ b/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(); diff --git a/README.md b/README.md index 55d6917ae0..5e731d2100 100644 --- a/README.md +++ b/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 diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs new file mode 100644 index 0000000000..1084e20bda --- /dev/null +++ b/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); + } + } +} diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookResolverTests.cs new file mode 100644 index 0000000000..83d44721c4 --- /dev/null +++ b/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 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); + } + } +}