diff options
25 files changed, 635 insertions, 69 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f0724b412..f1fe65064 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 f585b90ca..ed20292f6 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 3c874c62c..eb9393b0b 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 5466b4637..ed53bd04f 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 642e2fdbe..276d0fe30 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; @@ -1022,6 +1027,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> /// <returns>IEnumerable{Assembly}.</returns> @@ -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/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 47ebe1254..7fc996821 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -107,7 +107,7 @@ "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", "TaskCleanLogs": "Nettoyer le répertoire des journaux", "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.", - "TaskRefreshLibrary": "Scanner toute les Bibliothèques", + "TaskRefreshLibrary": "Scanner toutes les Bibliothèques", "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.", "TaskRefreshChapterImages": "Extraire les images de chapitre", "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json index 94034962d..faee2519a 100644 --- a/Emby.Server.Implementations/Localization/Core/gl.json +++ b/Emby.Server.Implementations/Localization/Core/gl.json @@ -1,3 +1,11 @@ { - "Albums": "Álbumes" + "Albums": "Álbumes", + "Collections": "Colecións", + "ChapterNameValue": "Capítulos {0}", + "Channels": "Canles", + "CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}", + "Books": "Libros", + "AuthenticationSucceededWithUserName": "{0} autenticouse correctamente", + "Artists": "Artistas", + "Application": "Aplicativo" } diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index 9e3ecd5a8..a33953c27 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -84,8 +84,8 @@ "UserDeletedWithName": "사용자 {0} 삭제됨", "UserDownloadingItemWithValues": "{0}이(가) {1}을 다운로드 중입니다", "UserLockedOutWithName": "유저 {0} 은(는) 잠금처리 되었습니다", - "UserOfflineFromDevice": "{1}로부터 {0}의 연결이 끊겼습니다", - "UserOnlineFromDevice": "{0}은 {1}에서 온라인 상태입니다", + "UserOfflineFromDevice": "{1}에서 {0}의 연결이 끊킴", + "UserOnlineFromDevice": "{0}이 {1}으로 접속", "UserPasswordChangedWithName": "사용자 {0}의 비밀번호가 변경되었습니다", "UserPolicyUpdatedWithName": "{0}의 사용자 정책이 업데이트되었습니다", "UserStartedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생 중", diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index a97c2e17a..07a599121 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Lydavspilling startet", "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet", "NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp", - "NotificationOptionInstallationFailed": "Installasjonsfeil", + "NotificationOptionInstallationFailed": "Installasjonen feilet", "NotificationOptionNewLibraryContent": "Nytt innhold lagt til", "NotificationOptionPluginError": "Pluginfeil", "NotificationOptionPluginInstalled": "Plugin installert", diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json new file mode 100644 index 000000000..347ba5f97 --- /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 000000000..f190f8298 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -0,0 +1,117 @@ +{ + "Collections": "Bộ Sưu Tập", + "Favorites": "Yêu Thích", + "Folders": "Thư Mục", + "Genres": "Thể Loại", + "HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ", + "HeaderContinueWatching": "Xem Tiếp", + "HeaderLiveTV": "TV Trực Tiếp", + "Movies": "Phim", + "Photos": "Ảnh", + "Playlists": "Danh sách phát", + "Shows": "Chương Trình TV", + "Songs": "Các Bài Hát", + "Sync": "Đồng Bộ", + "ValueSpecialEpisodeName": "Đặc Biệt - {0}", + "Albums": "Albums", + "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 video có các phân cảnh.", + "TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh", + "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": "Phân Cảnh {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": "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/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index 01108fe84..7b6540c3e 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -96,7 +96,7 @@ "TaskDownloadMissingSubtitles": "下載遺失的字幕", "TaskRefreshChannels": "重新整理頻道", "TaskUpdatePlugins": "更新外掛", - "TaskRefreshPeople": "重新整理人員", + "TaskRefreshPeople": "刷新用戶", "TaskCleanLogsDescription": "刪除超過 {0} 天的舊紀錄檔。", "TaskCleanLogs": "清空紀錄資料夾", "TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新描述資料。", diff --git a/Emby.Server.Implementations/Plugins/PluginManifest.cs b/Emby.Server.Implementations/Plugins/PluginManifest.cs new file mode 100644 index 000000000..33762791b --- /dev/null +++ b/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; } + } +} diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index e19158f8e..8e24bf55c 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.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 670b41611..7cf96dd34 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; @@ -113,7 +113,6 @@ namespace Jellyfin.Api.Controllers /// Gets a video hls playlist stream. /// </summary> /// <param name="itemId">The item id.</param> - /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param> /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param> /// <param name="params">The streaming parameters.</param> /// <param name="tag">The tag.</param> @@ -170,7 +169,6 @@ namespace Jellyfin.Api.Controllers [ProducesPlaylistFile] public async Task<ActionResult> GetMasterHlsVideoPlaylist( [FromRoute, Required] Guid itemId, - [FromRoute, Required] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -223,7 +221,6 @@ namespace Jellyfin.Api.Controllers var streamingRequest = new HlsVideoRequestDto { Id = itemId, - Container = container, Static = @static ?? true, Params = @params, Tag = tag, @@ -281,7 +278,6 @@ namespace Jellyfin.Api.Controllers /// Gets an audio hls playlist stream. /// </summary> /// <param name="itemId">The item id.</param> - /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param> /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param> /// <param name="params">The streaming parameters.</param> /// <param name="tag">The tag.</param> @@ -338,7 +334,6 @@ namespace Jellyfin.Api.Controllers [ProducesPlaylistFile] public async Task<ActionResult> GetMasterHlsAudioPlaylist( [FromRoute, Required] Guid itemId, - [FromQuery, Required] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -391,7 +386,6 @@ namespace Jellyfin.Api.Controllers var streamingRequest = new HlsAudioRequestDto { Id = itemId, - Container = container, Static = @static ?? true, Params = @params, Tag = tag, @@ -449,7 +443,6 @@ namespace Jellyfin.Api.Controllers /// Gets a video stream using HTTP live streaming. /// </summary> /// <param name="itemId">The item id.</param> - /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param> /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param> /// <param name="params">The streaming parameters.</param> /// <param name="tag">The tag.</param> @@ -504,7 +497,6 @@ namespace Jellyfin.Api.Controllers [ProducesPlaylistFile] public async Task<ActionResult> GetVariantHlsVideoPlaylist( [FromRoute, Required] Guid itemId, - [FromQuery, Required] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -557,7 +549,6 @@ namespace Jellyfin.Api.Controllers var streamingRequest = new VideoRequestDto { Id = itemId, - Container = container, Static = @static ?? true, Params = @params, Tag = tag, @@ -615,7 +606,6 @@ namespace Jellyfin.Api.Controllers /// Gets an audio stream using HTTP live streaming. /// </summary> /// <param name="itemId">The item id.</param> - /// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param> /// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param> /// <param name="params">The streaming parameters.</param> /// <param name="tag">The tag.</param> @@ -670,7 +660,6 @@ namespace Jellyfin.Api.Controllers [ProducesPlaylistFile] public async Task<ActionResult> GetVariantHlsAudioPlaylist( [FromRoute, Required] Guid itemId, - [FromQuery, Required] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -723,7 +712,6 @@ namespace Jellyfin.Api.Controllers var streamingRequest = new StreamingRequestDto { Id = itemId, - Container = container, Static = @static ?? true, Params = @params, Tag = tag, @@ -841,7 +829,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] string container, + [FromRoute] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -1011,7 +999,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] string container, + [FromRoute] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -1465,7 +1453,7 @@ namespace Jellyfin.Api.Controllers var args = "-codec:v:0 " + codec; - // if (state.EnableMpegtsM2TsMode) + // if (state.EnableMpegtsM2TsMode) // { // args += " -mpegts_m2ts_mode 1"; // } diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index b00675d67..a7bddc171 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -1,5 +1,3 @@ -#pragma warning disable CA1801 - using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -150,25 +148,25 @@ namespace Jellyfin.Api.Controllers /// Instructs a session to play an item. /// </summary> /// <param name="sessionId">The session id.</param> - /// <param name="command">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param> + /// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param> /// <param name="itemIds">The ids of the items to play, comma delimited.</param> /// <param name="startPositionTicks">The starting position of the first item.</param> /// <response code="204">Instruction sent to session.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> - [HttpPost("Sessions/{sessionId}/Playing/{command}")] + [HttpPost("Sessions/{sessionId}/Playing")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult Play( [FromRoute, Required] string sessionId, - [FromRoute, Required] PlayCommand command, - [FromQuery] Guid[] itemIds, + [FromQuery, Required] PlayCommand playCommand, + [FromQuery, Required] string itemIds, [FromQuery] long? startPositionTicks) { var playRequest = new PlayRequest { - ItemIds = itemIds, + ItemIds = RequestHelpers.GetGuids(itemIds), StartPositionTicks = startPositionTicks, - PlayCommand = command + PlayCommand = playCommand }; _sessionManager.SendPlayCommand( @@ -184,20 +182,29 @@ namespace Jellyfin.Api.Controllers /// Issues a playstate command to a client. /// </summary> /// <param name="sessionId">The session id.</param> - /// <param name="playstateRequest">The <see cref="PlaystateRequest"/>.</param> + /// <param name="command">The <see cref="PlaystateCommand"/>.</param> + /// <param name="seekPositionTicks">The optional position ticks.</param> + /// <param name="controllingUserId">The optional controlling user id.</param> /// <response code="204">Playstate command sent to session.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> - [HttpPost("Sessions/{sessionId}/Playing")] + [HttpPost("Sessions/{sessionId}/Playing/{command}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendPlaystateCommand( [FromRoute, Required] string sessionId, - [FromBody] PlaystateRequest playstateRequest) + [FromRoute, Required] PlaystateCommand command, + [FromQuery] long? seekPositionTicks, + [FromQuery] string? controllingUserId) { _sessionManager.SendPlaystateCommand( RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, - playstateRequest, + new PlaystateRequest() + { + Command = command, + ControllingUserId = controllingUserId, + SeekPositionTicks = seekPositionTicks, + }, CancellationToken.None); return NoContent(); @@ -434,9 +441,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult ReportViewing( [FromQuery] string? sessionId, - [FromQuery] string? itemId) + [FromQuery, Required] string? itemId) { - string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; + string session = sessionId ?? RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; _sessionManager.ReportNowViewingItem(session, itemId); return NoContent(); diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 89ab2da62..f4ec29bde 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -169,7 +169,7 @@ namespace Jellyfin.Api.Helpers string? containerInternal = Path.GetExtension(state.RequestedUrl); - if (string.IsNullOrEmpty(streamingRequest.Container)) + if (!string.IsNullOrEmpty(streamingRequest.Container)) { containerInternal = streamingRequest.Container; } diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index c71c76f08..f86b14244 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -20,8 +20,8 @@ <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.NativeAssets.Linux" Version="2.80.1" /> + <PackageReference Include="SkiaSharp" Version="2.80.2" /> + <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index c8bf5557b..3287f9814 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -212,7 +212,10 @@ namespace MediaBrowser.MediaEncoding.Encoder if (match.Success) { - return new Version(match.Groups[1].Value); + if (Version.TryParse(match.Groups[1].Value, out var result)) + { + return result; + } } var versionMap = GetFFmpegLibraryVersions(output); diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs index c088d8cec..5fa8a3e1c 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/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs index e7e2fd05b..dcc7f8700 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs @@ -112,9 +112,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV private async Task<List<Poster>> FetchImages(Season item, string tmdbId, string language, CancellationToken cancellationToken) { - await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, item.IndexNumber.GetValueOrDefault(), language, cancellationToken).ConfigureAwait(false); + var seasonNumber = item.IndexNumber.GetValueOrDefault(); + await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, seasonNumber, language, cancellationToken).ConfigureAwait(false); - var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language); + var path = TmdbSeasonProvider.Current.GetDataFilePath(tmdbId, seasonNumber, language); if (!string.IsNullOrEmpty(path)) { @@ -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/fedora/jellyfin.spec b/fedora/jellyfin.spec index 37b573e50..bfb2b3be2 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -84,6 +84,10 @@ EOF %{_libdir}/jellyfin/*.so %{_libdir}/jellyfin/*.a %{_libdir}/jellyfin/createdump +%{_libdir}/jellyfin/*.xml +%{_libdir}/jellyfin/wwwroot/api-docs/* +%{_libdir}/jellyfin/wwwroot/api-docs/redoc/* +%{_libdir}/jellyfin/wwwroot/api-docs/swagger/* # Needs 755 else only root can run it since binary build by dotnet is 722 %attr(755,root,root) %{_libdir}/jellyfin/jellyfin %{_libdir}/jellyfin/SOS_README.md diff --git a/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs b/tests/Jellyfin.Naming.Tests/AudioBook/AudioBookListResolverTests.cs new file mode 100644 index 000000000..1084e20bd --- /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 000000000..83d44721c --- /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<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); + } + } +} |
