diff --git a/Dockerfile b/Dockerfile index 6e834d4e0b..d3fb138a81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ ARG DOTNET_VERSION=3.1 FROM node:alpine as web-builder ARG JELLYFIN_WEB_VERSION=master -RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm \ +RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ && yarn install \ diff --git a/Dockerfile.arm b/Dockerfile.arm index 39beaa4791..59b8a8c982 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -38,7 +38,7 @@ COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \ curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \ - curl -s https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \ + curl -ks https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \ echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \ echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \ apt-get update && \ diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 7f755fd25e..948fe037b5 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -227,7 +227,7 @@ namespace Emby.Naming.Video } return remainingFiles - .Where(i => i.ExtraType == null) + .Where(i => i.ExtraType != null) .Where(i => baseNames.Any(b => i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase))) .ToList(); diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 4685a03ac3..3983824a3e 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -4,11 +4,10 @@ using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; @@ -30,7 +29,7 @@ namespace Emby.Server.Implementations.Activity /// public sealed class ActivityLogEntryPoint : IServerEntryPoint { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IInstallationManager _installationManager; private readonly ISessionManager _sessionManager; private readonly ITaskManager _taskManager; @@ -38,14 +37,12 @@ namespace Emby.Server.Implementations.Activity private readonly ILocalizationManager _localization; private readonly ISubtitleManager _subManager; private readonly IUserManager _userManager; - private readonly IDeviceManager _deviceManager; /// /// Initializes a new instance of the class. /// /// The logger. /// The session manager. - /// The device manager. /// The task manager. /// The activity manager. /// The localization manager. @@ -55,7 +52,6 @@ namespace Emby.Server.Implementations.Activity public ActivityLogEntryPoint( ILogger logger, ISessionManager sessionManager, - IDeviceManager deviceManager, ITaskManager taskManager, IActivityManager activityManager, ILocalizationManager localization, @@ -65,7 +61,6 @@ namespace Emby.Server.Implementations.Activity { _logger = logger; _sessionManager = sessionManager; - _deviceManager = deviceManager; _taskManager = taskManager; _activityManager = activityManager; _localization = localization; @@ -99,52 +94,38 @@ namespace Emby.Server.Implementations.Activity _userManager.UserPolicyUpdated += OnUserPolicyUpdated; _userManager.UserLockedOut += OnUserLockedOut; - _deviceManager.CameraImageUploaded += OnCameraImageUploaded; - return Task.CompletedTask; } - private void OnCameraImageUploaded(object sender, GenericEventArgs e) + private async void OnUserLockedOut(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("CameraImageUploadedFrom"), - e.Argument.Device.Name), - Type = NotificationType.CameraImageUploaded.ToString() - }); + await CreateLogEntry(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserLockedOutWithName"), + e.Argument.Name), + NotificationType.UserLockedOut.ToString(), + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnUserLockedOut(object sender, GenericEventArgs e) + private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserLockedOutWithName"), - e.Argument.Name), - Type = NotificationType.UserLockedOut.ToString(), - UserId = e.Argument.Id - }); - } - - private void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), e.Provider, Notifications.NotificationEntryPoint.GetItemName(e.Item)), - Type = "SubtitleDownloadFailure", + "SubtitleDownloadFailure", + Guid.Empty) + { ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture), ShortOverview = e.Exception.Message - }); + }).ConfigureAwait(false); } - private void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) + private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) { var item = e.MediaInfo; @@ -167,20 +148,19 @@ namespace Emby.Server.Implementations.Activity var user = e.Users[0]; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), - Type = GetPlaybackStoppedNotificationType(item.MediaType), - UserId = user.Id - }); + GetPlaybackStoppedNotificationType(item.MediaType), + user.Id)) + .ConfigureAwait(false); } - private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) + private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) { var item = e.MediaInfo; @@ -203,17 +183,16 @@ namespace Emby.Server.Implementations.Activity var user = e.Users.First(); - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserStartedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), - Type = GetPlaybackNotificationType(item.MediaType), - UserId = user.Id - }); + GetPlaybackNotificationType(item.MediaType), + user.Id)) + .ConfigureAwait(false); } private static string GetItemName(BaseItemDto item) @@ -263,7 +242,7 @@ namespace Emby.Server.Implementations.Activity return null; } - private void OnSessionEnded(object sender, SessionEventArgs e) + private async void OnSessionEnded(object sender, SessionEventArgs e) { var session = e.SessionInfo; @@ -272,110 +251,108 @@ namespace Emby.Server.Implementations.Activity return; } - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserOfflineFromDevice"), session.UserName, session.DeviceName), - Type = "SessionEnded", + "SessionEnded", + session.UserId) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint), - UserId = session.UserId - }); + }).ConfigureAwait(false); } - private void OnAuthenticationSucceeded(object sender, GenericEventArgs e) + private async void OnAuthenticationSucceeded(object sender, GenericEventArgs e) { var user = e.Argument.User; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("AuthenticationSucceededWithUserName"), user.Name), - Type = "AuthenticationSucceeded", + "AuthenticationSucceeded", + user.Id) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.SessionInfo.RemoteEndPoint), - UserId = user.Id - }); + }).ConfigureAwait(false); } - private void OnAuthenticationFailed(object sender, GenericEventArgs e) + private async void OnAuthenticationFailed(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("FailedLoginAttemptWithUserName"), e.Argument.Username), - Type = "AuthenticationFailed", + "AuthenticationFailed", + Guid.Empty) + { + LogSeverity = LogLevel.Error, ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.RemoteEndPoint), - Severity = LogLevel.Error - }); + }).ConfigureAwait(false); } - private void OnUserPolicyUpdated(object sender, GenericEventArgs e) + private async void OnUserPolicyUpdated(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserPolicyUpdatedWithName"), e.Argument.Name), - Type = "UserPolicyUpdated", - UserId = e.Argument.Id - }); + "UserPolicyUpdated", + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnUserDeleted(object sender, GenericEventArgs e) + private async void OnUserDeleted(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDeletedWithName"), e.Argument.Name), - Type = "UserDeleted" - }); + "UserDeleted", + Guid.Empty)) + .ConfigureAwait(false); } - private void OnUserPasswordChanged(object sender, GenericEventArgs e) + private async void OnUserPasswordChanged(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserPasswordChangedWithName"), e.Argument.Name), - Type = "UserPasswordChanged", - UserId = e.Argument.Id - }); + "UserPasswordChanged", + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnUserCreated(object sender, GenericEventArgs e) + private async void OnUserCreated(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserCreatedWithName"), e.Argument.Name), - Type = "UserCreated", - UserId = e.Argument.Id - }); + "UserCreated", + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnSessionStarted(object sender, SessionEventArgs e) + private async void OnSessionStarted(object sender, SessionEventArgs e) { var session = e.SessionInfo; @@ -384,87 +361,90 @@ namespace Emby.Server.Implementations.Activity return; } - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserOnlineFromDevice"), session.UserName, session.DeviceName), - Type = "SessionStarted", + "SessionStarted", + session.UserId) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), - session.RemoteEndPoint), - UserId = session.UserId - }); + session.RemoteEndPoint) + }).ConfigureAwait(false); } - private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e) + private async void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginUpdatedWithName"), e.Argument.Item1.Name), - Type = NotificationType.PluginUpdateInstalled.ToString(), + NotificationType.PluginUpdateInstalled.ToString(), + Guid.Empty) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), e.Argument.Item2.version), Overview = e.Argument.Item2.changelog - }); + }).ConfigureAwait(false); } - private void OnPluginUninstalled(object sender, GenericEventArgs e) + private async void OnPluginUninstalled(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginUninstalledWithName"), e.Argument.Name), - Type = NotificationType.PluginUninstalled.ToString() - }); + NotificationType.PluginUninstalled.ToString(), + Guid.Empty)) + .ConfigureAwait(false); } - private void OnPluginInstalled(object sender, GenericEventArgs e) + private async void OnPluginInstalled(object sender, GenericEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginInstalledWithName"), e.Argument.name), - Type = NotificationType.PluginInstalled.ToString(), + NotificationType.PluginInstalled.ToString(), + Guid.Empty) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), e.Argument.version) - }); + }).ConfigureAwait(false); } - private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) + private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) { var installationInfo = e.InstallationInfo; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("NameInstallFailed"), installationInfo.Name), - Type = NotificationType.InstallationFailed.ToString(), + NotificationType.InstallationFailed.ToString(), + Guid.Empty) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), installationInfo.Version), Overview = e.Exception.Message - }); + }).ConfigureAwait(false); } - private void OnTaskCompleted(object sender, TaskCompletionEventArgs e) + private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e) { var result = e.Result; var task = e.Task; @@ -495,22 +475,20 @@ namespace Emby.Server.Implementations.Activity vals.Add(e.Result.LongErrorMessage); } - CreateLogEntry(new ActivityLogEntry + await CreateLogEntry(new ActivityLog( + string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), + NotificationType.TaskFailed.ToString(), + Guid.Empty) { - Name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("ScheduledTaskFailedWithName"), - task.Name), - Type = NotificationType.TaskFailed.ToString(), + LogSeverity = LogLevel.Error, Overview = string.Join(Environment.NewLine, vals), - ShortOverview = runningTime, - Severity = LogLevel.Error - }); + ShortOverview = runningTime + }).ConfigureAwait(false); } } - private void CreateLogEntry(ActivityLogEntry entry) - => _activityManager.Create(entry); + private async Task CreateLogEntry(ActivityLog entry) + => await _activityManager.CreateAsync(entry).ConfigureAwait(false); /// public void Dispose() @@ -537,8 +515,6 @@ namespace Emby.Server.Implementations.Activity _userManager.UserDeleted -= OnUserDeleted; _userManager.UserPolicyUpdated -= OnUserPolicyUpdated; _userManager.UserLockedOut -= OnUserLockedOut; - - _deviceManager.CameraImageUploaded -= OnCameraImageUploaded; } /// @@ -566,7 +542,7 @@ namespace Emby.Server.Implementations.Activity { int months = days / DaysInMonth; values.Add(CreateValueString(months, "month")); - days %= DaysInMonth; + days = days % DaysInMonth; } // Number of days diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs deleted file mode 100644 index 81bebae3d2..0000000000 --- a/Emby.Server.Implementations/Activity/ActivityManager.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Querying; - -namespace Emby.Server.Implementations.Activity -{ - /// - /// The activity log manager. - /// - public class ActivityManager : IActivityManager - { - private readonly IActivityRepository _repo; - private readonly IUserManager _userManager; - - /// - /// Initializes a new instance of the class. - /// - /// The activity repository. - /// The user manager. - public ActivityManager(IActivityRepository repo, IUserManager userManager) - { - _repo = repo; - _userManager = userManager; - } - - /// - public event EventHandler> EntryCreated; - - public void Create(ActivityLogEntry entry) - { - entry.Date = DateTime.UtcNow; - - _repo.Create(entry); - - EntryCreated?.Invoke(this, new GenericEventArgs(entry)); - } - - /// - public QueryResult GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) - { - var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit); - - foreach (var item in result.Items) - { - if (item.UserId == Guid.Empty) - { - continue; - } - - var user = _userManager.GetUserById(item.UserId); - - if (user != null) - { - var dto = _userManager.GetUserDto(user); - item.UserPrimaryImageTag = dto.PrimaryImageTag; - } - } - - return result; - } - - /// - public QueryResult GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit) - { - return GetActivityLogEntries(minDate, null, startIndex, limit); - } - } -} diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs deleted file mode 100644 index 22796ba3f8..0000000000 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ /dev/null @@ -1,308 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using Emby.Server.Implementations.Data; -using MediaBrowser.Controller; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Querying; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Activity -{ - /// - /// The activity log repository. - /// - public class ActivityRepository : BaseSqliteRepository, IActivityRepository - { - private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog"; - - private readonly IFileSystem _fileSystem; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The server application paths. - /// The filesystem. - public ActivityRepository(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem) - : base(logger) - { - DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db"); - _fileSystem = fileSystem; - } - - /// - /// Initializes the . - /// - public void Initialize() - { - try - { - InitializeInternal(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error loading database file. Will reset and retry."); - - _fileSystem.DeleteFile(DbFilePath); - - InitializeInternal(); - } - } - - private void InitializeInternal() - { - using var connection = GetConnection(); - connection.RunQueries(new[] - { - "create table if not exists ActivityLog (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Overview TEXT, ShortOverview TEXT, Type TEXT NOT NULL, ItemId TEXT, UserId TEXT, DateCreated DATETIME NOT NULL, LogSeverity TEXT NOT NULL)", - "drop index if exists idx_ActivityLogEntries" - }); - - TryMigrate(connection); - } - - private void TryMigrate(ManagedConnection connection) - { - try - { - if (TableExists(connection, "ActivityLogEntries")) - { - connection.RunQueries(new[] - { - "INSERT INTO ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) SELECT Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity FROM ActivityLogEntries", - "drop table if exists ActivityLogEntries" - }); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error migrating activity log database"); - } - } - - /// - public void Create(ActivityLogEntry entry) - { - if (entry == null) - { - throw new ArgumentNullException(nameof(entry)); - } - - using var connection = GetConnection(); - connection.RunInTransaction(db => - { - using var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"); - statement.TryBind("@Name", entry.Name); - - statement.TryBind("@Overview", entry.Overview); - statement.TryBind("@ShortOverview", entry.ShortOverview); - statement.TryBind("@Type", entry.Type); - statement.TryBind("@ItemId", entry.ItemId); - - if (entry.UserId.Equals(Guid.Empty)) - { - statement.TryBindNull("@UserId"); - } - else - { - statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); - } - - statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); - statement.TryBind("@LogSeverity", entry.Severity.ToString()); - - statement.MoveNext(); - }, TransactionMode); - } - - /// - /// Adds the provided to this repository. - /// - /// The activity log entry. - /// If entry is null. - public void Update(ActivityLogEntry entry) - { - if (entry == null) - { - throw new ArgumentNullException(nameof(entry)); - } - - using var connection = GetConnection(); - connection.RunInTransaction(db => - { - using var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id"); - statement.TryBind("@Id", entry.Id); - - statement.TryBind("@Name", entry.Name); - statement.TryBind("@Overview", entry.Overview); - statement.TryBind("@ShortOverview", entry.ShortOverview); - statement.TryBind("@Type", entry.Type); - statement.TryBind("@ItemId", entry.ItemId); - - if (entry.UserId.Equals(Guid.Empty)) - { - statement.TryBindNull("@UserId"); - } - else - { - statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); - } - - statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); - statement.TryBind("@LogSeverity", entry.Severity.ToString()); - - statement.MoveNext(); - }, TransactionMode); - } - - /// - public QueryResult GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) - { - var commandText = BaseActivitySelectText; - var whereClauses = new List(); - - if (minDate.HasValue) - { - whereClauses.Add("DateCreated>=@DateCreated"); - } - - if (hasUserId.HasValue) - { - whereClauses.Add(hasUserId.Value ? "UserId not null" : "UserId is null"); - } - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - if (startIndex.HasValue && startIndex.Value > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - whereClauses.Add( - string.Format( - CultureInfo.InvariantCulture, - "Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})", - pagingWhereText, - startIndex.Value)); - } - - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - commandText += whereText; - - commandText += " ORDER BY DateCreated DESC"; - - if (limit.HasValue) - { - commandText += " LIMIT " + limit.Value.ToString(CultureInfo.InvariantCulture); - } - - var statementTexts = new[] - { - commandText, - "select count (Id) from ActivityLog" + whereTextWithoutPaging - }; - - var list = new List(); - var result = new QueryResult(); - - using var connection = GetConnection(true); - connection.RunInTransaction( - db => - { - var statements = PrepareAll(db, statementTexts).ToList(); - - using (var statement = statements[0]) - { - if (minDate.HasValue) - { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } - - list.AddRange(statement.ExecuteQuery().Select(GetEntry)); - } - - using (var statement = statements[1]) - { - if (minDate.HasValue) - { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } - - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); - } - }, - ReadTransactionMode); - - result.Items = list; - return result; - } - - private static ActivityLogEntry GetEntry(IReadOnlyList reader) - { - var index = 0; - - var info = new ActivityLogEntry - { - Id = reader[index].ToInt64() - }; - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Name = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Overview = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.ShortOverview = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Type = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.ItemId = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.UserId = new Guid(reader[index].ToString()); - } - - index++; - info.Date = reader[index].ReadDateTime(); - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Severity = Enum.Parse(reader[index].ToString(), true); - } - - return info; - } - } -} diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index f10981ef0f..be4e05a648 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -22,7 +22,6 @@ using Emby.Dlna.Ssdp; using Emby.Drawing; using Emby.Notifications; using Emby.Photos; -using Emby.Server.Implementations.Activity; using Emby.Server.Implementations.Archiving; using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; @@ -83,7 +82,6 @@ using MediaBrowser.Controller.TV; using MediaBrowser.Controller.SyncPlay; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; -using MediaBrowser.Model.Activity; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dlna; @@ -632,9 +630,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -665,7 +660,6 @@ namespace Emby.Server.Implementations ((SqliteDisplayPreferencesRepository)Resolve()).Initialize(); ((AuthenticationRepository)Resolve()).Initialize(); ((SqliteUserRepository)Resolve()).Initialize(); - ((ActivityRepository)Resolve()).Initialize(); SetStaticProperties(); diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index a6eaf2d0a3..305e67e8c3 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -193,12 +193,6 @@ namespace Emby.Server.Implementations.Configuration changed = true; } - if (!config.CameraUploadUpgraded) - { - config.CameraUploadUpgraded = true; - changed = true; - } - if (!config.CollectionsUpgraded) { config.CollectionsUpgraded = true; diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index 579cb895e4..2283f2433a 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -5,27 +5,18 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; using MediaBrowser.Model.Users; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Devices { @@ -33,38 +24,23 @@ namespace Emby.Server.Implementations.Devices { private readonly IJsonSerializer _json; private readonly IUserManager _userManager; - private readonly IFileSystem _fileSystem; - private readonly ILibraryMonitor _libraryMonitor; private readonly IServerConfigurationManager _config; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localizationManager; private readonly IAuthenticationRepository _authRepo; private readonly Dictionary _capabilitiesCache; public event EventHandler>> DeviceOptionsUpdated; - public event EventHandler> CameraImageUploaded; - - private readonly object _cameraUploadSyncLock = new object(); private readonly object _capabilitiesSyncLock = new object(); public DeviceManager( IAuthenticationRepository authRepo, IJsonSerializer json, - ILibraryManager libraryManager, - ILocalizationManager localizationManager, IUserManager userManager, - IFileSystem fileSystem, - ILibraryMonitor libraryMonitor, IServerConfigurationManager config) { _json = json; _userManager = userManager; - _fileSystem = fileSystem; - _libraryMonitor = libraryMonitor; _config = config; - _libraryManager = libraryManager; - _localizationManager = localizationManager; _authRepo = authRepo; _capabilitiesCache = new Dictionary(StringComparer.OrdinalIgnoreCase); } @@ -194,172 +170,6 @@ namespace Emby.Server.Implementations.Devices return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture)); } - public ContentUploadHistory GetCameraUploadHistory(string deviceId) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - - lock (_cameraUploadSyncLock) - { - try - { - return _json.DeserializeFromFile(path); - } - catch (IOException) - { - return new ContentUploadHistory - { - DeviceId = deviceId - }; - } - } - } - - public async Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file) - { - var device = GetDevice(deviceId, false); - var uploadPathInfo = GetUploadPath(device); - - var path = uploadPathInfo.Item1; - - if (!string.IsNullOrWhiteSpace(file.Album)) - { - path = Path.Combine(path, _fileSystem.GetValidFilename(file.Album)); - } - - path = Path.Combine(path, file.Name); - path = Path.ChangeExtension(path, MimeTypes.ToExtension(file.MimeType) ?? "jpg"); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - await EnsureLibraryFolder(uploadPathInfo.Item2, uploadPathInfo.Item3).ConfigureAwait(false); - - _libraryMonitor.ReportFileSystemChangeBeginning(path); - - try - { - using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - await stream.CopyToAsync(fs).ConfigureAwait(false); - } - - AddCameraUpload(deviceId, file); - } - finally - { - _libraryMonitor.ReportFileSystemChangeComplete(path, true); - } - - if (CameraImageUploaded != null) - { - CameraImageUploaded?.Invoke(this, new GenericEventArgs - { - Argument = new CameraImageUploadInfo - { - Device = device, - FileInfo = file - } - }); - } - } - - private void AddCameraUpload(string deviceId, LocalFileInfo file) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_cameraUploadSyncLock) - { - ContentUploadHistory history; - - try - { - history = _json.DeserializeFromFile(path); - } - catch (IOException) - { - history = new ContentUploadHistory - { - DeviceId = deviceId - }; - } - - history.DeviceId = deviceId; - - var list = history.FilesUploaded.ToList(); - list.Add(file); - history.FilesUploaded = list.ToArray(); - - _json.SerializeToFile(history, path); - } - } - - internal Task EnsureLibraryFolder(string path, string name) - { - var existingFolders = _libraryManager - .RootFolder - .Children - .OfType() - .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path)) - .ToList(); - - if (existingFolders.Count > 0) - { - return Task.CompletedTask; - } - - Directory.CreateDirectory(path); - - var libraryOptions = new LibraryOptions - { - PathInfos = new[] { new MediaPathInfo { Path = path } }, - EnablePhotos = true, - EnableRealtimeMonitor = false, - SaveLocalMetadata = true - }; - - if (string.IsNullOrWhiteSpace(name)) - { - name = _localizationManager.GetLocalizedString("HeaderCameraUploads"); - } - - return _libraryManager.AddVirtualFolder(name, CollectionType.HomeVideos, libraryOptions, true); - } - - private Tuple GetUploadPath(DeviceInfo device) - { - var config = _config.GetUploadOptions(); - var path = config.CameraUploadPath; - - if (string.IsNullOrWhiteSpace(path)) - { - path = DefaultCameraUploadsPath; - } - - var topLibraryPath = path; - - if (config.EnableCameraUploadSubfolders) - { - path = Path.Combine(path, _fileSystem.GetValidFilename(device.Name)); - } - - return new Tuple(path, topLibraryPath, null); - } - - internal string GetUploadsPath() - { - var config = _config.GetUploadOptions(); - var path = config.CameraUploadPath; - - if (string.IsNullOrWhiteSpace(path)) - { - path = DefaultCameraUploadsPath; - } - - return path; - } - - private string DefaultCameraUploadsPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads"); - public bool CanAccessDevice(User user, string deviceId) { if (user == null) @@ -399,102 +209,4 @@ namespace Emby.Server.Implementations.Devices return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase); } } - - public class DeviceManagerEntryPoint : IServerEntryPoint - { - private readonly DeviceManager _deviceManager; - private readonly IServerConfigurationManager _config; - private ILogger _logger; - - public DeviceManagerEntryPoint( - IDeviceManager deviceManager, - IServerConfigurationManager config, - ILogger logger) - { - _deviceManager = (DeviceManager)deviceManager; - _config = config; - _logger = logger; - } - - public async Task RunAsync() - { - if (!_config.Configuration.CameraUploadUpgraded && _config.Configuration.IsStartupWizardCompleted) - { - var path = _deviceManager.GetUploadsPath(); - - if (Directory.Exists(path)) - { - try - { - await _deviceManager.EnsureLibraryFolder(path, null).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating camera uploads library"); - } - - _config.Configuration.CameraUploadUpgraded = true; - _config.SaveConfiguration(); - } - } - } - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects). - } - - // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. - // TODO: set large fields to null. - - disposedValue = true; - } - } - - // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. - // ~DeviceManagerEntryPoint() { - // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - // Dispose(false); - // } - - // This code added to correctly implement the disposable pattern. - public void Dispose() - { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - Dispose(true); - // TODO: uncomment the following line if the finalizer is overridden above. - // GC.SuppressFinalize(this); - } - #endregion - } - - public class DevicesConfigStore : IConfigurationFactory - { - public IEnumerable GetConfigurations() - { - return new ConfigurationStore[] - { - new ConfigurationStore - { - Key = "devices", - ConfigurationType = typeof(DevicesOptions) - } - }; - } - } - - public static class UploadConfigExtension - { - public static DevicesOptions GetUploadOptions(this IConfigurationManager config) - { - return config.GetConfiguration("devices"); - } - } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 44fc932e39..b69a126b3a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -1,4 +1,4 @@ - + @@ -34,15 +34,16 @@ - - - - + + + + + diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 794d55c049..7de4f168c1 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -210,16 +210,8 @@ namespace Emby.Server.Implementations.HttpServer } } - private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog) + private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace) { - bool ignoreStackTrace = - ex is SocketException - || ex is IOException - || ex is OperationCanceledException - || ex is SecurityException - || ex is AuthenticationException - || ex is FileNotFoundException; - if (ignoreStackTrace) { _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog); @@ -505,14 +497,32 @@ namespace Emby.Server.Implementations.HttpServer var requestInnerEx = GetActualException(requestEx); var statusCode = GetStatusCode(requestInnerEx); - // Do not handle 500 server exceptions manually when in development mode - // The framework-defined development exception page will be returned instead - if (statusCode == 500 && _hostEnvironment.IsDevelopment()) + foreach (var (key, value) in GetDefaultCorsHeaders(httpReq)) + { + if (!httpRes.Headers.ContainsKey(key)) + { + httpRes.Headers.Add(key, value); + } + } + + bool ignoreStackTrace = + requestInnerEx is SocketException + || requestInnerEx is IOException + || requestInnerEx is OperationCanceledException + || requestInnerEx is SecurityException + || requestInnerEx is AuthenticationException + || requestInnerEx is FileNotFoundException; + + // Do not handle 500 server exceptions manually when in development mode. + // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware. + // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored, + // because it will log the stack trace when it handles the exception. + if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment()) { throw; } - await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog).ConfigureAwait(false); + await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false); } catch (Exception handlerException) { diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 5a1eb43bcb..eb5e190aab 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -11,6 +11,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.IO; +using Emby.Server.Implementations.Library; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO @@ -37,38 +38,6 @@ namespace Emby.Server.Implementations.IO /// private readonly ConcurrentDictionary _tempIgnoredPaths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - /// - /// Any file name ending in any of these will be ignored by the watchers. - /// - private static readonly HashSet _alwaysIgnoreFiles = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "small.jpg", - "albumart.jpg", - - // WMC temp recording directories that will constantly be written to - "TempRec", - "TempSBE" - }; - - private static readonly string[] _alwaysIgnoreSubstrings = new string[] - { - // Synology - "eaDir", - "#recycle", - ".wd_tv", - ".actors" - }; - - private static readonly HashSet _alwaysIgnoreExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) - { - // thumbs.db - ".db", - - // bts sync files - ".bts", - ".sync" - }; - /// /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. /// @@ -395,12 +364,7 @@ namespace Emby.Server.Implementations.IO throw new ArgumentNullException(nameof(path)); } - var filename = Path.GetFileName(path); - - var monitorPath = !string.IsNullOrEmpty(filename) && - !_alwaysIgnoreFiles.Contains(filename) && - !_alwaysIgnoreExtensions.Contains(Path.GetExtension(path)) && - _alwaysIgnoreSubstrings.All(i => path.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1); + var monitorPath = !IgnorePatterns.ShouldIgnore(path); // Ignore certain files var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList(); diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index bc1398332d..218e5a0c6d 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -1,7 +1,5 @@ using System; using System.IO; -using System.Linq; -using System.Text.RegularExpressions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; @@ -16,32 +14,6 @@ namespace Emby.Server.Implementations.Library { private readonly ILibraryManager _libraryManager; - /// - /// Any folder named in this list will be ignored - /// - private static readonly string[] _ignoreFolders = - { - "metadata", - "ps3_update", - "ps3_vprm", - "extrafanart", - "extrathumbs", - ".actors", - ".wd_tv", - - // Synology - "@eaDir", - "eaDir", - "#recycle", - - // Qnap - "@Recycle", - ".@__thumb", - "$RECYCLE.BIN", - "System Volume Information", - ".grab", - }; - /// /// Initializes a new instance of the class. /// @@ -60,23 +32,15 @@ namespace Emby.Server.Implementations.Library return false; } - var filename = fileInfo.Name; - - // Ignore hidden files on UNIX - if (Environment.OSVersion.Platform != PlatformID.Win32NT - && filename[0] == '.') + if (IgnorePatterns.ShouldIgnore(fileInfo.FullName)) { return true; } + var filename = fileInfo.Name; + if (fileInfo.IsDirectory) { - // Ignore any folders in our list - if (_ignoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase)) - { - return true; - } - if (parent != null) { // Ignore trailer folders but allow it at the collection level @@ -109,11 +73,6 @@ namespace Emby.Server.Implementations.Library return true; } } - - // Ignore samples - Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase); - - return m.Success; } return false; diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs new file mode 100644 index 0000000000..d12b5855b2 --- /dev/null +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -0,0 +1,74 @@ +using System.Linq; +using DotNet.Globbing; + +namespace Emby.Server.Implementations.Library +{ + /// + /// Glob patterns for files to ignore + /// + public static class IgnorePatterns + { + /// + /// Files matching these glob patterns will be ignored + /// + public static readonly string[] Patterns = new string[] + { + "**/small.jpg", + "**/albumart.jpg", + "**/*sample*", + + // Directories + "**/metadata/**", + "**/ps3_update/**", + "**/ps3_vprm/**", + "**/extrafanart/**", + "**/extrathumbs/**", + "**/.actors/**", + "**/.wd_tv/**", + "**/lost+found/**", + + // WMC temp recording directories that will constantly be written to + "**/TempRec/**", + "**/TempSBE/**", + + // Synology + "**/eaDir/**", + "**/@eaDir/**", + "**/#recycle/**", + + // Qnap + "**/@Recycle/**", + "**/.@__thumb/**", + "**/$RECYCLE.BIN/**", + "**/System Volume Information/**", + "**/.grab/**", + + // Unix hidden files and directories + "**/.*/**", + + // thumbs.db + "**/thumbs.db", + + // bts sync files + "**/*.bts", + "**/*.sync", + }; + + private static readonly GlobOptions _globOptions = new GlobOptions + { + Evaluation = { + CaseInsensitive = true + } + }; + + private static readonly Glob[] _globs = Patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray(); + + /// + /// Returns true if the supplied path should be ignored + /// + public static bool ShouldIgnore(string path) + { + return _globs.Any(g => g.IsMatch(path)); + } + } +} diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 0b93ebeb81..503de0b4e9 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver { - private readonly string[] _validExtensions = { ".pdf", ".epub", ".mobi", ".cbr", ".cbz", ".azw3" }; + private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" }; protected override Book Resolve(ItemResolveArgs args) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 083fcd0299..322fbbbaa5 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Net.Http; using System.Threading; @@ -118,6 +119,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts //OpenedMediaSource.SupportsDirectStream = true; //OpenedMediaSource.SupportsTranscoding = true; await taskCompletionSource.Task.ConfigureAwait(false); + if (taskCompletionSource.Task.Exception != null) + { + // Error happened while opening the stream so raise the exception again to inform the caller + throw taskCompletionSource.Task.Exception; + } + + if (!taskCompletionSource.Task.Result) + { + Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath); + throw new EndOfStreamException(String.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name)); + } } private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken) @@ -139,14 +151,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts cancellationToken).ConfigureAwait(false); } } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { + Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath); + openTaskCompletionSource.TrySetException(ex); } catch (Exception ex) { - Logger.LogError(ex, "Error copying live stream."); + Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath); + openTaskCompletionSource.TrySetException(ex); } + openTaskCompletionSource.TrySetResult(false); + EnableStreamSharing = false; await DeleteTempFiles(new List { TempFilePath }).ConfigureAwait(false); }); diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index f313039a69..d68928fce4 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -9,7 +9,7 @@ "Channels": "القنوات", "ChapterNameValue": "الفصل {0}", "Collections": "مجموعات", - "DeviceOfflineWithName": "قُطِع الاتصال بـ{0}", + "DeviceOfflineWithName": "قُطِع الاتصال ب{0}", "DeviceOnlineWithName": "{0} متصل", "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}", "Favorites": "المفضلة", diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 992bb9df37..464ca28ca0 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -23,7 +23,7 @@ "HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteSongs": "Oblíbená hudba", - "HeaderLiveTV": "Živá TV", + "HeaderLiveTV": "Televize", "HeaderNextUp": "Nadcházející", "HeaderRecordingGroups": "Skupiny nahrávek", "HomeVideos": "Domáci videa", diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index 1b6c6b5ae4..fc9a10f276 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -24,7 +24,7 @@ "HeaderFavoriteShows": "Programas favoritos", "HeaderFavoriteSongs": "Canciones favoritas", "HeaderLiveTV": "TV en vivo", - "HeaderNextUp": "A Continuación", + "HeaderNextUp": "Siguiente", "HeaderRecordingGroups": "Grupos de grabación", "HomeVideos": "Videos caseros", "Inherit": "Heredar", @@ -44,7 +44,7 @@ "NameInstallFailed": "{0} instalación fallida", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada desconocida", - "NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.", + "NewVersionIsAvailable": "Una nueva versión del servidor Jellyfin está disponible para descargar.", "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible", "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada", "NotificationOptionAudioPlayback": "Se inició la reproducción de audio", @@ -56,7 +56,7 @@ "NotificationOptionPluginInstalled": "Complemento instalado", "NotificationOptionPluginUninstalled": "Complemento desinstalado", "NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada", - "NotificationOptionServerRestartRequired": "Se necesita reiniciar el Servidor", + "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor", "NotificationOptionTaskFailed": "Falla de tarea programada", "NotificationOptionUserLockedOut": "Usuario bloqueado", "NotificationOptionVideoPlayback": "Se inició la reproducción de video", @@ -71,7 +71,7 @@ "ScheduledTaskFailedWithName": "{0} falló", "ScheduledTaskStartedWithName": "{0} iniciado", "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", - "Shows": "Series", + "Shows": "Programas", "Songs": "Canciones", "StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", @@ -94,25 +94,25 @@ "ValueSpecialEpisodeName": "Especial - {0}", "VersionNumber": "Versión {0}", "TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten basándose en la configuración de los metadatos.", - "TaskDownloadMissingSubtitles": "Descargar subtítulos extraviados", + "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes", "TaskRefreshChannelsDescription": "Actualizar información de canales de internet.", "TaskRefreshChannels": "Actualizar canales", "TaskCleanTranscodeDescription": "Eliminar archivos transcodificados con mas de un día de antigüedad.", - "TaskCleanTranscode": "Limpiar directorio de Transcodificado", + "TaskCleanTranscode": "Limpiar directorio de transcodificación", "TaskUpdatePluginsDescription": "Descargar e instalar actualizaciones para complementos que estén configurados en actualizar automáticamente.", "TaskUpdatePlugins": "Actualizar complementos", - "TaskRefreshPeopleDescription": "Actualizar metadatos de actores y directores en su librería multimedia.", + "TaskRefreshPeopleDescription": "Actualizar metadatos de actores y directores en su biblioteca multimedia.", "TaskRefreshPeople": "Actualizar personas", "TaskCleanLogsDescription": "Eliminar archivos de registro que tengan mas de {0} días de antigüedad.", "TaskCleanLogs": "Limpiar directorio de registros", - "TaskRefreshLibraryDescription": "Escanear su librería multimedia por nuevos archivos y refrescar metadatos.", - "TaskRefreshLibrary": "Escanear librería multimedia", + "TaskRefreshLibraryDescription": "Escanear su biblioteca multimedia por nuevos archivos y refrescar metadatos.", + "TaskRefreshLibrary": "Escanear biblioteca multimedia", "TaskRefreshChapterImagesDescription": "Crear miniaturas de videos que tengan capítulos.", - "TaskRefreshChapterImages": "Extraer imágenes de capitulo", - "TaskCleanCacheDescription": "Eliminar archivos de cache que no se necesiten en el sistema.", - "TaskCleanCache": "Limpiar directorio Cache", - "TasksChannelsCategory": "Canales de Internet", - "TasksApplicationCategory": "Solicitud", + "TaskRefreshChapterImages": "Extraer imágenes de capítulo", + "TaskCleanCacheDescription": "Eliminar archivos de caché que no se necesiten en el sistema.", + "TaskCleanCache": "Limpiar directorio caché", + "TasksChannelsCategory": "Canales de internet", + "TasksApplicationCategory": "Aplicación", "TasksLibraryCategory": "Biblioteca", "TasksMaintenanceCategory": "Mantenimiento" } diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index d93920f433..20b37ec9f2 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -16,16 +16,16 @@ "Folders": "Carpetas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas del álbum", - "HeaderCameraUploads": "Subidos desde Camara", - "HeaderContinueWatching": "Continuar Viendo", + "HeaderCameraUploads": "Subidas desde la cámara", + "HeaderContinueWatching": "Continuar viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteEpisodes": "Episodios favoritos", "HeaderFavoriteShows": "Programas favoritos", "HeaderFavoriteSongs": "Canciones favoritas", - "HeaderLiveTV": "TV en Vivo", - "HeaderNextUp": "A Continuación", - "HeaderRecordingGroups": "Grupos de Grabaciones", + "HeaderLiveTV": "TV en vivo", + "HeaderNextUp": "A continuación", + "HeaderRecordingGroups": "Grupos de grabación", "HomeVideos": "Videos caseros", "Inherit": "Heredar", "ItemAddedWithName": "{0} fue agregado a la biblioteca", @@ -41,12 +41,12 @@ "Movies": "Películas", "Music": "Música", "MusicVideos": "Videos musicales", - "NameInstallFailed": "{0} instalación fallida", + "NameInstallFailed": "Falló la instalación de {0}", "NameSeasonNumber": "Temporada {0}", - "NameSeasonUnknown": "Temporada Desconocida", + "NameSeasonUnknown": "Temporada desconocida", "NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.", - "NotificationOptionApplicationUpdateAvailable": "Actualización de aplicación disponible", - "NotificationOptionApplicationUpdateInstalled": "Actualización de aplicación instalada", + "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible", + "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada", "NotificationOptionAudioPlayback": "Reproducción de audio iniciada", "NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida", "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida", @@ -56,7 +56,7 @@ "NotificationOptionPluginInstalled": "Complemento instalado", "NotificationOptionPluginUninstalled": "Complemento desinstalado", "NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada", - "NotificationOptionServerRestartRequired": "Se necesita reiniciar el Servidor", + "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor", "NotificationOptionTaskFailed": "Falla de tarea programada", "NotificationOptionUserLockedOut": "Usuario bloqueado", "NotificationOptionVideoPlayback": "Reproducción de video iniciada", @@ -69,48 +69,48 @@ "PluginUpdatedWithName": "{0} fue actualizado", "ProviderValue": "Proveedor: {0}", "ScheduledTaskFailedWithName": "{0} falló", - "ScheduledTaskStartedWithName": "{0} Iniciado", + "ScheduledTaskStartedWithName": "{0} iniciado", "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado", "Shows": "Programas", "Songs": "Canciones", - "StartupEmbyServerIsLoading": "El servidor Jellyfin esta cargando. Por favor intente de nuevo dentro de poco.", + "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.", "SubtitleDownloadFailureForItem": "Falló la descarga de subtítulos para {0}", - "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}", + "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}", "Sync": "Sincronizar", "System": "Sistema", "TvShows": "Programas de TV", "User": "Usuario", - "UserCreatedWithName": "Se ha creado el usuario {0}", - "UserDeletedWithName": "Se ha eliminado el usuario {0}", - "UserDownloadingItemWithValues": "{0} esta descargando {1}", + "UserCreatedWithName": "El usuario {0} ha sido creado", + "UserDeletedWithName": "El usuario {0} ha sido eliminado", + "UserDownloadingItemWithValues": "{0} está descargando {1}", "UserLockedOutWithName": "El usuario {0} ha sido bloqueado", "UserOfflineFromDevice": "{0} se ha desconectado desde {1}", "UserOnlineFromDevice": "{0} está en línea desde {1}", "UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}", - "UserPolicyUpdatedWithName": "Las política de usuario ha sido actualizada por {0}", - "UserStartedPlayingItemWithValues": "{0} está reproduciéndose {1} en {2}", - "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducirse {1} en {2}", - "ValueHasBeenAddedToLibrary": "{0} se han añadido a su biblioteca de medios", + "UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}", + "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}", + "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}", + "ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca de medios", "ValueSpecialEpisodeName": "Especial - {0}", "VersionNumber": "Versión {0}", - "TaskDownloadMissingSubtitlesDescription": "Buscar subtítulos de internet basado en configuración de metadatos.", - "TaskDownloadMissingSubtitles": "Descargar subtítulos perdidos", - "TaskRefreshChannelsDescription": "Refrescar información de canales de internet.", + "TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.", + "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes", + "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.", "TaskRefreshChannels": "Actualizar canales", - "TaskCleanTranscodeDescription": "Eliminar archivos transcodificados que tengan mas de un día.", + "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.", "TaskCleanTranscode": "Limpiar directorio de transcodificado", - "TaskUpdatePluginsDescription": "Descargar y actualizar complementos que están configurados para actualizarse automáticamente.", + "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.", "TaskUpdatePlugins": "Actualizar complementos", - "TaskRefreshPeopleDescription": "Actualizar datos de actores y directores en su librería multimedia.", - "TaskRefreshPeople": "Refrescar persona", - "TaskCleanLogsDescription": "Eliminar archivos de registro con mas de {0} días.", - "TaskCleanLogs": "Directorio de logo limpio", - "TaskRefreshLibraryDescription": "Escanear su librería multimedia para nuevos archivos y refrescar metadatos.", - "TaskRefreshLibrary": "Escanear librería multimerdia", - "TaskRefreshChapterImagesDescription": "Crear miniaturas para videos con capítulos.", - "TaskRefreshChapterImages": "Extraer imágenes de capítulos", - "TaskCleanCacheDescription": "Eliminar archivos cache que ya no se necesiten por el sistema.", - "TaskCleanCache": "Limpiar directorio cache", + "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.", + "TaskRefreshPeople": "Actualizar personas", + "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad.", + "TaskCleanLogs": "Limpiar directorio de registros", + "TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios por archivos nuevos y actualiza los metadatos.", + "TaskRefreshLibrary": "Escanear biblioteca de medios", + "TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.", + "TaskRefreshChapterImages": "Extraer imágenes de los capítulos", + "TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.", + "TaskCleanCache": "Limpiar directorio caché", "TasksChannelsCategory": "Canales de Internet", "TasksApplicationCategory": "Aplicación", "TasksLibraryCategory": "Biblioteca", diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index c2349ba5bb..3dcfa68441 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -96,21 +96,22 @@ "TasksLibraryCategory": "Bibliothèque", "TasksMaintenanceCategory": "Entretien", "TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.", - "TaskDownloadMissingSubtitles": "Télécharger des sous-titres manquants", - "TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines d'internet.", + "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants", + "TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines internet.", "TaskRefreshChannels": "Rafraîchir des chaines", - "TaskCleanTranscodeDescription": "Retirer des fichiers de transcodage de plus qu'un jour.", - "TaskCleanTranscode": "Nettoyer le directoire de transcodage", - "TaskUpdatePluginsDescription": "Télécharger et installer des mises à jours des plugins qui sont configurés m.à.j. automisés.", - "TaskUpdatePlugins": "Mise à jour des plugins", - "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.", + "TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage de plus d'un jour.", + "TaskCleanTranscode": "Nettoyer le répertoire de transcodage", + "TaskUpdatePluginsDescription": "Télécharger et installer les mises à jours des extensions qui sont configurés pour les m.à.j. automisés.", + "TaskUpdatePlugins": "Mise à jour des extensions", + "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque de médias.", "TaskRefreshPeople": "Rafraîchir les acteurs", - "TaskCleanLogsDescription": "Retire les données qui ont plus que {0} jours.", - "TaskCleanLogs": "Nettoyer les données de directoire", - "TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour des nouveaux fichiers et rafraîchit les métadonnées.", - "TaskRefreshChapterImages": "Extraire des images du chapitre", - "TaskRefreshChapterImagesDescription": "Créer des vignettes pour des vidéos qui ont des chapitres", - "TaskRefreshLibrary": "Analyser la bibliothèque de média", - "TaskCleanCache": "Nettoyer le cache de directoire", - "TasksApplicationCategory": "Application" + "TaskCleanLogsDescription": "Supprime les journaux qui ont plus que {0} jours.", + "TaskCleanLogs": "Nettoyer le répertoire des journaux", + "TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour trouver de nouveaux fichiers et rafraîchit les métadonnées.", + "TaskRefreshChapterImages": "Extraire les images de chapitre", + "TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres", + "TaskRefreshLibrary": "Analyser la bibliothèque de médias", + "TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires", + "TasksApplicationCategory": "Application", + "TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système." } diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index 4e54b9f7ad..682f5325b5 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -107,5 +107,12 @@ "TaskCleanLogs": "נקה תיקיית יומן", "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.", "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.", - "TasksChannelsCategory": "ערוצי אינטרנט" + "TasksChannelsCategory": "ערוצי אינטרנט", + "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.", + "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.", + "TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.", + "TaskRefreshChannels": "רענן ערוץ", + "TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.", + "TaskCleanTranscode": "נקה תקיית Transcode", + "TaskUpdatePluginsDescription": "הורד והתקן עדכונים עבור תוספים שמוגדרים לעדכון אוטומטי." } diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index ef2a57e8e8..0f0f9130b0 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -80,16 +80,32 @@ "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt", "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}", "UserStartedPlayingItemWithValues": "{0} er að spila {1} á {2}", - "UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir notanda {0}", + "UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir {0}", "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt", "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}", "UserOfflineFromDevice": "{0} hefur aftengst frá {1}", - "UserLockedOutWithName": "Notanda {0} hefur verið hindraður aðgangur", + "UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur", "UserDownloadingItemWithValues": "{0} Hleður niður {1}", "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}", "ProviderValue": "Veitandi: {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón", "ValueSpecialEpisodeName": "Sérstakt - {0}", - "Shows": "Þættir", - "Playlists": "Spilunarlisti" + "Shows": "Sýningar", + "Playlists": "Spilunarlisti", + "TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.", + "TaskRefreshChannels": "Endurhlaða Rásir", + "TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.", + "TaskCleanTranscode": "Hreinsa Umkóðunarmöppu", + "TaskUpdatePluginsDescription": "Sækja og setja upp uppfærslur fyrir viðbætur sem eru stilltar til að uppfæra sjálfkrafa.", + "TaskUpdatePlugins": "Uppfæra viðbætur", + "TaskRefreshPeopleDescription": "Uppfærir lýsigögn fyrir leikara og leikstjóra í miðlasafninu þínu.", + "TaskRefreshLibraryDescription": "Skannar miðlasafnið þitt fyrir nýjum skrám og uppfærir lýsigögn.", + "TaskRefreshLibrary": "Skanna miðlasafn", + "TaskRefreshChapterImagesDescription": "Býr til smámyndir fyrir myndbönd sem hafa kaflaskil.", + "TaskCleanCacheDescription": "Eyðir skrám í skyndiminni sem ekki er lengur þörf fyrir í kerfinu.", + "TaskCleanCache": "Hreinsa skráasafn skyndiminnis", + "TasksChannelsCategory": "Netrásir", + "TasksApplicationCategory": "Forrit", + "TasksLibraryCategory": "Miðlasafn", + "TasksMaintenanceCategory": "Viðhald" } diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 01a740187d..35053766b4 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}", "ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką", "ValueSpecialEpisodeName": "Ypatinga - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.", + "TaskUpdatePlugins": "Atnaujinti Priedus", + "TaskDownloadMissingSubtitlesDescription": "Ieško internete trūkstamų subtitrų remiantis metaduomenų konfigūracija.", + "TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.", + "TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija", + "TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.", + "TaskRefreshLibrary": "Skenuoti Mediateka", + "TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus", + "TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informacija.", + "TaskRefreshChannels": "Atnaujinti Kanalus", + "TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.", + "TaskRefreshPeople": "Atnaujinti Žmones", + "TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.", + "TaskCleanLogs": "Išvalyti Žurnalą", + "TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašam, kurie turi scenas.", + "TaskRefreshChapterImages": "Ištraukti Scenų Paveikslus", + "TaskCleanCache": "Išvalyti Talpyklą", + "TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.", + "TasksChannelsCategory": "Internetiniai Kanalai", + "TasksApplicationCategory": "Programa", + "TasksLibraryCategory": "Mediateka", + "TasksMaintenanceCategory": "Priežiūra" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 3a69b6d7a5..275195640b 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -19,10 +19,10 @@ "HeaderCameraUploads": "Envios da Câmera", "HeaderContinueWatching": "Continuar Assistindo", "HeaderFavoriteAlbums": "Álbuns Favoritos", - "HeaderFavoriteArtists": "Artistas Favoritos", - "HeaderFavoriteEpisodes": "Episódios Favoritos", - "HeaderFavoriteShows": "Séries Favoritas", - "HeaderFavoriteSongs": "Músicas Favoritas", + "HeaderFavoriteArtists": "Artistas favoritos", + "HeaderFavoriteEpisodes": "Episódios favoritos", + "HeaderFavoriteShows": "Séries favoritas", + "HeaderFavoriteSongs": "Músicas favoritas", "HeaderLiveTV": "TV ao Vivo", "HeaderNextUp": "A Seguir", "HeaderRecordingGroups": "Grupos de Gravação", diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json new file mode 100644 index 0000000000..32538ac035 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -0,0 +1,71 @@ +{ + "ProviderValue": "ผู้ให้บริการ: {0}", + "PluginUpdatedWithName": "{0} ได้รับการ update แล้ว", + "PluginUninstalledWithName": "ถอนการติดตั้ง {0}", + "PluginInstalledWithName": "{0} ได้รับการติดตั้ง", + "Plugin": "Plugin", + "Playlists": "รายการ", + "Photos": "รูปภาพ", + "NotificationOptionVideoPlaybackStopped": "หยุดการเล่น Video", + "NotificationOptionVideoPlayback": "เริ่มแสดง Video", + "NotificationOptionUserLockedOut": "ผู้ใช้ Locked Out", + "NotificationOptionTaskFailed": "ตารางการทำงานล้มเหลว", + "NotificationOptionServerRestartRequired": "ควร Restart Server", + "NotificationOptionPluginUpdateInstalled": "Update Plugin แล้ว", + "NotificationOptionPluginUninstalled": "ถอด Plugin", + "NotificationOptionPluginInstalled": "ติดตั้ง Plugin แล้ว", + "NotificationOptionPluginError": "Plugin ล้มเหลว", + "NotificationOptionNewLibraryContent": "เพิ่มข้อมูลใหม่แล้ว", + "NotificationOptionInstallationFailed": "ติดตั้งล้มเหลว", + "NotificationOptionCameraImageUploaded": "รูปภาพถูก upload", + "NotificationOptionAudioPlaybackStopped": "หยุดการเล่นเสียง", + "NotificationOptionAudioPlayback": "เริ่มเล่นเสียง", + "NotificationOptionApplicationUpdateInstalled": "Update ระบบแล้ว", + "NotificationOptionApplicationUpdateAvailable": "ระบบ update สามารถใช้ได้แล้ว", + "NewVersionIsAvailable": "ตรวจพบ Jellyfin เวอร์ชั่นใหม่", + "NameSeasonUnknown": "ไม่ทราบปี", + "NameSeasonNumber": "ปี {0}", + "NameInstallFailed": "{0} ติดตั้งไม่สำเร็จ", + "MusicVideos": "MV", + "Music": "เพลง", + "Movies": "ภาพยนต์", + "MixedContent": "รายการแบบผสม", + "MessageServerConfigurationUpdated": "การตั้งค่า update แล้ว", + "MessageNamedServerConfigurationUpdatedWithValue": "รายการตั้งค่า {0} ได้รับการ update แล้ว", + "MessageApplicationUpdatedTo": "Jellyfin Server จะ update ไปที่ {0}", + "MessageApplicationUpdated": "Jellyfin Server update แล้ว", + "Latest": "ล่าสุด", + "LabelRunningTimeValue": "เวลาที่เล่น : {0}", + "LabelIpAddressValue": "IP address: {0}", + "ItemRemovedWithName": "{0} ถูกลบจากรายการ", + "ItemAddedWithName": "{0} ถูกเพิ่มในรายการ", + "Inherit": "การสืบทอด", + "HomeVideos": "วีดีโอส่วนตัว", + "HeaderRecordingGroups": "ค่ายบันทึก", + "HeaderNextUp": "ถัดไป", + "HeaderLiveTV": "รายการสด", + "HeaderFavoriteSongs": "เพลงโปรด", + "HeaderFavoriteShows": "รายการโชว์โปรด", + "HeaderFavoriteEpisodes": "ฉากโปรด", + "HeaderFavoriteArtists": "นักแสดงโปรด", + "HeaderFavoriteAlbums": "อัมบั้มโปรด", + "HeaderContinueWatching": "ชมต่อจากเดิม", + "HeaderCameraUploads": "Upload รูปภาพ", + "HeaderAlbumArtists": "อัลบั้มนักแสดง", + "Genres": "ประเภท", + "Folders": "โฟลเดอร์", + "Favorites": "รายการโปรด", + "FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}", + "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ", + "DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ", + "Collections": "ชุด", + "ChapterNameValue": "บทที่ {0}", + "Channels": "ชาแนล", + "CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}", + "Books": "หนังสือ", + "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ", + "Artists": "นักแสดง", + "Application": "แอปพลิเคชั่น", + "AppDeviceValues": "App: {0}, อุปกรณ์: {1}", + "Albums": "อัลบั้ม" +} diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index a67a67582f..0804fc9279 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -11,15 +11,15 @@ "Collections": "合輯", "DeviceOfflineWithName": "{0} 已經斷開連結", "DeviceOnlineWithName": "{0} 已經連接", - "FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試", + "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗", "Favorites": "我的最愛", "Folders": "檔案夾", "Genres": "風格", - "HeaderAlbumArtists": "專輯藝術家", + "HeaderAlbumArtists": "專輯藝人", "HeaderCameraUploads": "相機上載", "HeaderContinueWatching": "繼續觀看", "HeaderFavoriteAlbums": "最愛專輯", - "HeaderFavoriteArtists": "最愛藝術家", + "HeaderFavoriteArtists": "最愛的藝人", "HeaderFavoriteEpisodes": "最愛的劇集", "HeaderFavoriteShows": "最愛的節目", "HeaderFavoriteSongs": "最愛的歌曲", @@ -33,14 +33,14 @@ "LabelIpAddressValue": "IP 地址: {0}", "LabelRunningTimeValue": "運行時間: {0}", "Latest": "最新", - "MessageApplicationUpdated": "Jellyfin Server 已更新", + "MessageApplicationUpdated": "Jellyfin 伺服器已更新", "MessageApplicationUpdatedTo": "Jellyfin 伺服器已更新至 {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已更新", + "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已更新", "MessageServerConfigurationUpdated": "伺服器設定已經更新", - "MixedContent": "Mixed content", + "MixedContent": "混合內容", "Movies": "電影", "Music": "音樂", - "MusicVideos": "音樂MV", + "MusicVideos": "音樂視頻", "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知季數", @@ -49,7 +49,7 @@ "NotificationOptionApplicationUpdateInstalled": "應用程式已更新", "NotificationOptionAudioPlayback": "開始播放音頻", "NotificationOptionAudioPlaybackStopped": "已停止播放音頻", - "NotificationOptionCameraImageUploaded": "相機相片已上傳", + "NotificationOptionCameraImageUploaded": "相片已上傳", "NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionNewLibraryContent": "已添加新内容", "NotificationOptionPluginError": "擴充元件錯誤", @@ -63,11 +63,11 @@ "NotificationOptionVideoPlaybackStopped": "已停止播放視頻", "Photos": "相片", "Playlists": "播放清單", - "Plugin": "Plugin", + "Plugin": "插件", "PluginInstalledWithName": "已安裝 {0}", "PluginUninstalledWithName": "已移除 {0}", "PluginUpdatedWithName": "已更新 {0}", - "ProviderValue": "Provider: {0}", + "ProviderValue": "提供者: {0}", "ScheduledTaskFailedWithName": "{0} 任務失敗", "ScheduledTaskStartedWithName": "{0} 任務開始", "ServerNameNeedsToBeRestarted": "{0} 需要重啓", @@ -77,17 +77,17 @@ "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕", "Sync": "同步", - "System": "System", + "System": "系統", "TvShows": "電視節目", - "User": "User", - "UserCreatedWithName": "用家 {0} 已創建", - "UserDeletedWithName": "用家 {0} 已移除", + "User": "使用者", + "UserCreatedWithName": "使用者 {0} 已創建", + "UserDeletedWithName": "使用者 {0} 已移除", "UserDownloadingItemWithValues": "{0} 正在下載 {1}", - "UserLockedOutWithName": "用家 {0} 已被鎖定", + "UserLockedOutWithName": "使用者 {0} 已被鎖定", "UserOfflineFromDevice": "{0} 已從 {1} 斷開", "UserOnlineFromDevice": "{0} 已連綫,來自 {1}", - "UserPasswordChangedWithName": "用家 {0} 的密碼已變更", - "UserPolicyUpdatedWithName": "用戶協議已被更新為 {0}", + "UserPasswordChangedWithName": "使用者 {0} 的密碼已變更", + "UserPolicyUpdatedWithName": "使用者協議已更新為 {0}", "UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}", "UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫", @@ -95,5 +95,23 @@ "VersionNumber": "版本{0}", "TaskDownloadMissingSubtitles": "下載遺失的字幕", "TaskUpdatePlugins": "更新插件", - "TasksApplicationCategory": "應用程式" + "TasksApplicationCategory": "應用程式", + "TaskRefreshLibraryDescription": "掃描媒體庫以查找新文件並刷新metadata。", + "TasksMaintenanceCategory": "維護", + "TaskDownloadMissingSubtitlesDescription": "根據metadata配置在互聯網上搜索缺少的字幕。", + "TaskRefreshChannelsDescription": "刷新互聯網頻道信息。", + "TaskRefreshChannels": "刷新頻道", + "TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。", + "TaskCleanTranscode": "清理轉碼目錄", + "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。", + "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的metadata。", + "TaskCleanLogsDescription": "刪除超過{0}天的日誌文件。", + "TaskCleanLogs": "清理日誌目錄", + "TaskRefreshLibrary": "掃描媒體庫", + "TaskRefreshChapterImagesDescription": "為帶有章節的視頻創建縮略圖。", + "TaskRefreshChapterImages": "提取章節圖像", + "TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。", + "TaskCleanCache": "清理緩存目錄", + "TasksChannelsCategory": "互聯網頻道", + "TasksLibraryCategory": "庫" } diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 26f7d9d2dd..aab1141ee8 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -1,3 +1,4 @@ +using System.Security.Authentication; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; @@ -59,6 +60,10 @@ namespace Jellyfin.Api.Auth return Task.FromResult(AuthenticateResult.Success(ticket)); } + catch (AuthenticationException ex) + { + return Task.FromResult(AuthenticateResult.Fail(ex)); + } catch (SecurityException ex) { return Task.FromResult(AuthenticateResult.Fail(ex)); diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index a582a209cb..25d5d0c893 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -13,7 +13,7 @@ - + diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs new file mode 100644 index 0000000000..522c206640 --- /dev/null +++ b/Jellyfin.Data/Entities/ActivityLog.cs @@ -0,0 +1,154 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Data.Entities +{ + /// + /// An entity referencing an activity log entry. + /// + public partial class ActivityLog : ISavingChanges + { + /// + /// Initializes a new instance of the class. + /// Public constructor with required data. + /// + /// The name. + /// The type. + /// The user id. + public ActivityLog(string name, string type, Guid userId) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentNullException(nameof(type)); + } + + this.Name = name; + this.Type = type; + this.UserId = userId; + this.DateCreated = DateTime.UtcNow; + this.LogSeverity = LogLevel.Trace; + + Init(); + } + + /// + /// Initializes a new instance of the class. + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// + protected ActivityLog() + { + Init(); + } + + /// + /// Static create function (for use in LINQ queries, etc.) + /// + /// The name. + /// The type. + /// The user's id. + /// The new instance. + public static ActivityLog Create(string name, string type, Guid userId) + { + return new ActivityLog(name, type, userId); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// + /// Gets or sets the identity of this instance. + /// This is the key in the backing database. + /// + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// + /// Gets or sets the name. + /// Required, Max length = 512. + /// + [Required] + [MaxLength(512)] + [StringLength(512)] + public string Name { get; set; } + + /// + /// Gets or sets the overview. + /// Max length = 512. + /// + [MaxLength(512)] + [StringLength(512)] + public string Overview { get; set; } + + /// + /// Gets or sets the short overview. + /// Max length = 512. + /// + [MaxLength(512)] + [StringLength(512)] + public string ShortOverview { get; set; } + + /// + /// Gets or sets the type. + /// Required, Max length = 256. + /// + [Required] + [MaxLength(256)] + [StringLength(256)] + public string Type { get; set; } + + /// + /// Gets or sets the user id. + /// Required. + /// + [Required] + public Guid UserId { get; set; } + + /// + /// Gets or sets the item id. + /// Max length = 256. + /// + [MaxLength(256)] + [StringLength(256)] + public string ItemId { get; set; } + + /// + /// Gets or sets the date created. This should be in UTC. + /// Required. + /// + [Required] + public DateTime DateCreated { get; set; } + + /// + /// Gets or sets the log severity. Default is . + /// Required. + /// + [Required] + public LogLevel LogSeverity { get; set; } + + /// + /// Gets or sets the row version. + /// Required, ConcurrencyToken. + /// + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + partial void Init(); + + /// + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 73ea593b0b..9157c3ead9 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -1,12 +1,26 @@ - netstandard2.0 + netstandard2.0;netstandard2.1 + false + true + + ../jellyfin.ruleset + + + + + + + + + + - - + + diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs new file mode 100644 index 0000000000..65ceee32bf --- /dev/null +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -0,0 +1,102 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Querying; + +namespace Jellyfin.Server.Implementations.Activity +{ + /// + /// Manages the storage and retrieval of instances. + /// + public class ActivityManager : IActivityManager + { + private readonly JellyfinDbProvider _provider; + + /// + /// Initializes a new instance of the class. + /// + /// The Jellyfin database provider. + public ActivityManager(JellyfinDbProvider provider) + { + _provider = provider; + } + + /// + public event EventHandler> EntryCreated; + + /// + public void Create(ActivityLog entry) + { + using var dbContext = _provider.CreateContext(); + dbContext.ActivityLogs.Add(entry); + dbContext.SaveChanges(); + + EntryCreated?.Invoke(this, new GenericEventArgs(ConvertToOldModel(entry))); + } + + /// + public async Task CreateAsync(ActivityLog entry) + { + using var dbContext = _provider.CreateContext(); + await dbContext.ActivityLogs.AddAsync(entry); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + EntryCreated?.Invoke(this, new GenericEventArgs(ConvertToOldModel(entry))); + } + + /// + public QueryResult GetPagedResult( + Func, IQueryable> func, + int? startIndex, + int? limit) + { + using var dbContext = _provider.CreateContext(); + + var query = func(dbContext.ActivityLogs.OrderByDescending(entry => entry.DateCreated)); + + if (startIndex.HasValue) + { + query = query.Skip(startIndex.Value); + } + + if (limit.HasValue) + { + query = query.Take(limit.Value); + } + + // This converts the objects from the new database model to the old for compatibility with the existing API. + var list = query.Select(ConvertToOldModel).ToList(); + + return new QueryResult + { + Items = list, + TotalRecordCount = func(dbContext.ActivityLogs).Count() + }; + } + + /// + public QueryResult GetPagedResult(int? startIndex, int? limit) + { + return GetPagedResult(logs => logs, startIndex, limit); + } + + private static ActivityLogEntry ConvertToOldModel(ActivityLog entry) + { + return new ActivityLogEntry + { + Id = entry.Id, + Name = entry.Name, + Overview = entry.Overview, + ShortOverview = entry.ShortOverview, + Type = entry.Type, + ItemId = entry.ItemId, + UserId = entry.UserId, + Date = entry.DateCreated, + Severity = entry.LogSeverity + }; + } + } +} diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index a31f28f64a..8486fc2dfb 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -25,6 +25,17 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 76343edf9d..ec09a619f2 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -15,6 +15,7 @@ namespace Jellyfin.Server.Implementations /// public partial class JellyfinDb : DbContext { + public virtual DbSet ActivityLogs { get; set; } /*public virtual DbSet Artwork { get; set; } public virtual DbSet Books { get; set; } public virtual DbSet BookMetadata { get; set; } @@ -49,6 +50,7 @@ namespace Jellyfin.Server.Implementations public virtual DbSet Preferences { get; set; } public virtual DbSet ProviderMappings { get; set; } public virtual DbSet Ratings { get; set; } + /// /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to /// store review ratings, not age ratings @@ -93,8 +95,10 @@ namespace Jellyfin.Server.Implementations modelBuilder.HasDefaultSchema("jellyfin"); /*modelBuilder.Entity().HasIndex(t => t.Kind); + modelBuilder.Entity().HasIndex(t => t.Name) .IsUnique(); + modelBuilder.Entity().HasIndex(t => t.UrlId) .IsUnique();*/ @@ -103,9 +107,10 @@ namespace Jellyfin.Server.Implementations public override int SaveChanges() { - foreach (var entity in ChangeTracker.Entries().Where(e => e.State == EntityState.Modified)) + foreach (var saveEntity in ChangeTracker.Entries() + .Where(e => e.State == EntityState.Modified) + .OfType()) { - var saveEntity = entity.Entity as ISavingChanges; saveEntity.OnSavingChanges(); } diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs new file mode 100644 index 0000000000..eab531d386 --- /dev/null +++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Server.Implementations +{ + /// + /// Factory class for generating new instances. + /// + public class JellyfinDbProvider + { + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The application's service provider. + public JellyfinDbProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + serviceProvider.GetService().Database.Migrate(); + } + + /// + /// Creates a new context. + /// + /// The newly created context. + public JellyfinDb CreateContext() + { + return _serviceProvider.GetRequiredService(); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs new file mode 100644 index 0000000000..98a83b7450 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs @@ -0,0 +1,72 @@ +#pragma warning disable CS1591 + +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20200514181226_AddActivityLog")] + partial class AddActivityLog + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs new file mode 100644 index 0000000000..5e0b454d8b --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs @@ -0,0 +1,46 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class AddActivityLog : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "jellyfin"); + + migrationBuilder.CreateTable( + name: "ActivityLogs", + schema: "jellyfin", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(maxLength: 512, nullable: false), + Overview = table.Column(maxLength: 512, nullable: true), + ShortOverview = table.Column(maxLength: 512, nullable: true), + Type = table.Column(maxLength: 256, nullable: false), + UserId = table.Column(nullable: false), + ItemId = table.Column(maxLength: 256, nullable: true), + DateCreated = table.Column(nullable: false), + LogSeverity = table.Column(nullable: false), + RowVersion = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ActivityLogs", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ActivityLogs", + schema: "jellyfin"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs new file mode 100644 index 0000000000..72a4a8c3b6 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + /// The design time factory for . + /// This is only used for the creation of migrations and not during runtime. + /// + internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory + { + public JellyfinDb CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseSqlite("Data Source=jellyfin.db"); + + return new JellyfinDb(optionsBuilder.Options); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs new file mode 100644 index 0000000000..1e7ffd2359 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -0,0 +1,66 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + partial class JellyfinDbModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index f678e714c1..331a32c737 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -1,12 +1,17 @@ using System; using System.Collections.Generic; +using System.IO; using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; +using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.Activity; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Activity; using MediaBrowser.Model.IO; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -56,6 +61,15 @@ namespace Jellyfin.Server Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}."); } + // TODO: Set up scoping and use AddDbContextPool + serviceCollection.AddDbContext( + options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), + ServiceLifetime.Transient); + + serviceCollection.AddSingleton(); + + serviceCollection.AddSingleton(); + base.RegisterServices(serviceCollection); } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 88114d9994..c93aa837e7 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -41,8 +41,8 @@ - - + + @@ -60,6 +60,7 @@ + diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index ca17482820..473f62737e 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -17,7 +17,9 @@ namespace Jellyfin.Server.Migrations private static readonly Type[] _migrationTypes = { typeof(Routines.DisableTranscodingThrottling), - typeof(Routines.CreateUserLoggingConfigFile) + typeof(Routines.CreateUserLoggingConfigFile), + typeof(Routines.MigrateActivityLogDb), + typeof(Routines.RemoveDuplicateExtras) }; /// diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs new file mode 100644 index 0000000000..b3cc297082 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Emby.Server.Implementations.Data; +using Jellyfin.Data.Entities; +using Jellyfin.Server.Implementations; +using MediaBrowser.Controller; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// + /// The migration routine for migrating the activity log database to EF Core. + /// + public class MigrateActivityLogDb : IMigrationRoutine + { + private const string DbFilename = "activitylog.db"; + + private readonly ILogger _logger; + private readonly JellyfinDbProvider _provider; + private readonly IServerApplicationPaths _paths; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The server application paths. + /// The database provider. + public MigrateActivityLogDb(ILogger logger, IServerApplicationPaths paths, JellyfinDbProvider provider) + { + _logger = logger; + _provider = provider; + _paths = paths; + } + + /// + public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978"); + + /// + public string Name => "MigrateActivityLogDatabase"; + + /// + public void Perform() + { + var logLevelDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "None", LogLevel.None }, + { "Trace", LogLevel.Trace }, + { "Debug", LogLevel.Debug }, + { "Information", LogLevel.Information }, + { "Info", LogLevel.Information }, + { "Warn", LogLevel.Warning }, + { "Warning", LogLevel.Warning }, + { "Error", LogLevel.Error }, + { "Critical", LogLevel.Critical } + }; + + var dataPath = _paths.DataPath; + using (var connection = SQLite3.Open( + Path.Combine(dataPath, DbFilename), + ConnectionFlags.ReadOnly, + null)) + { + _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin."); + using var dbContext = _provider.CreateContext(); + + var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id ASC"); + + // Make sure that the database is empty in case of failed migration due to power outages, etc. + dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs); + dbContext.SaveChanges(); + // Reset the autoincrement counter + dbContext.Database.ExecuteSqlRaw("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'ActivityLog';"); + dbContext.SaveChanges(); + + var newEntries = queryResult.Select(entry => + { + if (!logLevelDictionary.TryGetValue(entry[8].ToString(), out var severity)) + { + severity = LogLevel.Trace; + } + + var newEntry = new ActivityLog( + entry[1].ToString(), + entry[4].ToString(), + entry[6].SQLiteType == SQLiteType.Null ? Guid.Empty : Guid.Parse(entry[6].ToString())) + { + DateCreated = entry[7].ReadDateTime(), + LogSeverity = severity + }; + + if (entry[2].SQLiteType != SQLiteType.Null) + { + newEntry.Overview = entry[2].ToString(); + } + + if (entry[3].SQLiteType != SQLiteType.Null) + { + newEntry.ShortOverview = entry[3].ToString(); + } + + if (entry[5].SQLiteType != SQLiteType.Null) + { + newEntry.ItemId = entry[5].ToString(); + } + + return newEntry; + }); + + dbContext.ActivityLogs.AddRange(newEntries); + dbContext.SaveChanges(); + } + + try + { + File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old")); + + var journalPath = Path.Combine(dataPath, DbFilename + "-journal"); + if (File.Exists(journalPath)) + { + File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal")); + } + } + catch (IOException e) + { + _logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'"); + } + } + } +} diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs new file mode 100644 index 0000000000..e955363881 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs @@ -0,0 +1,79 @@ +using System; +using System.Globalization; +using System.IO; + +using MediaBrowser.Controller; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// + /// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself. + /// + internal class RemoveDuplicateExtras : IMigrationRoutine + { + private const string DbFilename = "library.db"; + private readonly ILogger _logger; + private readonly IServerApplicationPaths _paths; + + public RemoveDuplicateExtras(ILogger logger, IServerApplicationPaths paths) + { + _logger = logger; + _paths = paths; + } + + /// + public Guid Id => Guid.Parse("{ACBE17B7-8435-4A83-8B64-6FCF162CB9BD}"); + + /// + public string Name => "RemoveDuplicateExtras"; + + /// + public void Perform() + { + var dataPath = _paths.DataPath; + var dbPath = Path.Combine(dataPath, DbFilename); + using (var connection = SQLite3.Open( + dbPath, + ConnectionFlags.ReadWrite, + null)) + { + // Query the database for the ids of duplicate extras + var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'"); + var bads = string.Join(", ", queryResult.SelectScalarString()); + + // Do nothing if no duplicate extras were detected + if (bads.Length == 0) + { + _logger.LogInformation("No duplicate extras detected, skipping migration."); + return; + } + + // Back up the database before deleting any entries + for (int i = 1; ; i++) + { + var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i); + if (!File.Exists(bakPath)) + { + try + { + File.Copy(dbPath, bakPath); + _logger.LogInformation("Library database backed up to {BackupPath}", bakPath); + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath); + throw; + } + } + } + + // Delete all duplicate extras + _logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads); + connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')"); + } + } + } +} diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs index 7004a2559e..53eb9667d8 100644 --- a/MediaBrowser.Api/Devices/DeviceService.cs +++ b/MediaBrowser.Api/Devices/DeviceService.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Net; @@ -116,11 +115,6 @@ namespace MediaBrowser.Api.Devices return _deviceManager.GetDeviceOptions(request.Id); } - public object Get(GetCameraUploads request) - { - return ToOptimizedResult(_deviceManager.GetCameraUploadHistory(request.DeviceId)); - } - public void Delete(DeleteDevice request) { var sessions = _authRepo.Get(new AuthenticationInfoQuery @@ -134,35 +128,5 @@ namespace MediaBrowser.Api.Devices _sessionManager.Logout(session); } } - - public Task Post(PostCameraUpload request) - { - var deviceId = Request.QueryString["DeviceId"]; - var album = Request.QueryString["Album"]; - var id = Request.QueryString["Id"]; - var name = Request.QueryString["Name"]; - var req = Request.Response.HttpContext.Request; - - if (req.HasFormContentType) - { - var file = req.Form.Files.Count == 0 ? null : req.Form.Files[0]; - - return _deviceManager.AcceptCameraUpload(deviceId, file.OpenReadStream(), new LocalFileInfo - { - MimeType = file.ContentType, - Album = album, - Name = name, - Id = id - }); - } - - return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo - { - MimeType = Request.ContentType, - Album = album, - Name = name, - Id = id - }); - } } } diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 2d1977d2e3..c0146dfee6 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -763,13 +763,12 @@ namespace MediaBrowser.Api.Library { try { - _activityManager.Create(new ActivityLogEntry + _activityManager.Create(new Jellyfin.Data.Entities.ActivityLog( + string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name), + "UserDownloadingContent", + auth.UserId) { - Name = string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name), - Type = "UserDownloadingContent", ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device), - UserId = auth.UserId - }); } catch diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs index f95fa7ca0b..a6bacad4fc 100644 --- a/MediaBrowser.Api/System/ActivityLogService.cs +++ b/MediaBrowser.Api/System/ActivityLogService.cs @@ -1,5 +1,7 @@ using System; using System.Globalization; +using System.Linq; +using Jellyfin.Data.Entities; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; @@ -53,7 +55,10 @@ namespace MediaBrowser.Api.System (DateTime?)null : DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); - var result = _activityManager.GetActivityLogEntries(minDate, request.HasUserId, request.StartIndex, request.Limit); + var filterFunc = new Func, IQueryable>( + entries => entries.Where(entry => entry.DateCreated >= minDate)); + + var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit); return ToOptimizedResult(result); } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 69864106c7..a597b90524 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -17,8 +17,8 @@ - - + + diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 77d5676310..ef3f43c759 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,6 +1,4 @@ using System; -using System.IO; -using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Events; @@ -11,11 +9,6 @@ namespace MediaBrowser.Controller.Devices { public interface IDeviceManager { - /// - /// Occurs when [camera image uploaded]. - /// - event EventHandler> CameraImageUploaded; - /// /// Saves the capabilities. /// @@ -45,22 +38,6 @@ namespace MediaBrowser.Controller.Devices /// IEnumerable<DeviceInfo>. QueryResult GetDevices(DeviceQuery query); - /// - /// Gets the upload history. - /// - /// The device identifier. - /// ContentUploadHistory. - ContentUploadHistory GetCameraUploadHistory(string deviceId); - - /// - /// Accepts the upload. - /// - /// The device identifier. - /// The stream. - /// The file. - /// Task. - Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file); - /// /// Determines whether this instance [can access device] the specified user identifier. /// diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 4e7d027374..223bbe1ded 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs index 0b2f1d231e..a2ea0766a8 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -278,5 +278,19 @@ namespace MediaBrowser.MediaEncoding.Probing /// The disposition. [JsonPropertyName("disposition")] public IReadOnlyDictionary Disposition { get; set; } + + /// + /// Gets or sets the color transfer. + /// + /// The color transfer. + [JsonPropertyName("color_transfer")] + public string ColorTransfer { get; set; } + + /// + /// Gets or sets the color primaries. + /// + /// The color primaries. + [JsonPropertyName("color_primaries")] + public string ColorPrimaries { get; set; } } } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index b24d97f4ef..d3f8094b94 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -695,6 +695,16 @@ namespace MediaBrowser.MediaEncoding.Probing { stream.RefFrames = streamInfo.Refs; } + + if (!string.IsNullOrEmpty(streamInfo.ColorTransfer)) + { + stream.ColorTransfer = streamInfo.ColorTransfer; + } + + if (!string.IsNullOrEmpty(streamInfo.ColorPrimaries)) + { + stream.ColorPrimaries = streamInfo.ColorPrimaries; + } } else { diff --git a/MediaBrowser.Model/Activity/ActivityLogEntry.cs b/MediaBrowser.Model/Activity/ActivityLogEntry.cs index 80f01b66ee..5ab904394e 100644 --- a/MediaBrowser.Model/Activity/ActivityLogEntry.cs +++ b/MediaBrowser.Model/Activity/ActivityLogEntry.cs @@ -59,6 +59,7 @@ namespace MediaBrowser.Model.Activity /// Gets or sets the user primary image tag. /// /// The user primary image tag. + [Obsolete("UserPrimaryImageTag is not used.")] public string UserPrimaryImageTag { get; set; } /// diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index f336f5272c..9dab5e77b7 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -1,6 +1,9 @@ #pragma warning disable CS1591 using System; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; @@ -10,10 +13,15 @@ namespace MediaBrowser.Model.Activity { event EventHandler> EntryCreated; - void Create(ActivityLogEntry entry); + void Create(ActivityLog entry); - QueryResult GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit); + Task CreateAsync(ActivityLog entry); - QueryResult GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? x, int? y); + QueryResult GetPagedResult(int? startIndex, int? limit); + + QueryResult GetPagedResult( + Func, IQueryable> func, + int? startIndex, + int? limit); } } diff --git a/MediaBrowser.Model/Activity/IActivityRepository.cs b/MediaBrowser.Model/Activity/IActivityRepository.cs deleted file mode 100644 index 66144ec478..0000000000 --- a/MediaBrowser.Model/Activity/IActivityRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.Querying; - -namespace MediaBrowser.Model.Activity -{ - public interface IActivityRepository - { - void Create(ActivityLogEntry entry); - - QueryResult GetActivityLogEntries(DateTime? minDate, bool? z, int? startIndex, int? limit); - } -} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 22a42322a6..1f5981f101 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -79,8 +79,6 @@ namespace MediaBrowser.Model.Configuration public bool EnableRemoteAccess { get; set; } - public bool CameraUploadUpgraded { get; set; } - public bool CollectionsUpgraded { get; set; } /// diff --git a/MediaBrowser.Model/Devices/DeviceOptions.cs b/MediaBrowser.Model/Devices/DeviceOptions.cs new file mode 100644 index 0000000000..8b77fd7fc3 --- /dev/null +++ b/MediaBrowser.Model/Devices/DeviceOptions.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 + +namespace MediaBrowser.Model.Devices +{ + public class DeviceOptions + { + public string CustomName { get; set; } + } +} diff --git a/MediaBrowser.Model/Devices/DevicesOptions.cs b/MediaBrowser.Model/Devices/DevicesOptions.cs deleted file mode 100644 index 02570650e9..0000000000 --- a/MediaBrowser.Model/Devices/DevicesOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Devices -{ - public class DevicesOptions - { - public string[] EnabledCameraUploadDevices { get; set; } - public string CameraUploadPath { get; set; } - public bool EnableCameraUploadSubfolders { get; set; } - - public DevicesOptions() - { - EnabledCameraUploadDevices = Array.Empty(); - } - } - - public class DeviceOptions - { - public string CustomName { get; set; } - } -} diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index e7e8d7cecd..ac33f1da42 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -34,8 +34,22 @@ namespace MediaBrowser.Model.Entities /// The language. public string Language { get; set; } + /// + /// Gets or sets the color transfer. + /// + /// The color transfer. public string ColorTransfer { get; set; } + + /// + /// Gets or sets the color primaries. + /// + /// The color primaries. public string ColorPrimaries { get; set; } + + /// + /// Gets or sets the color space. + /// + /// The color space. public string ColorSpace { get; set; } /// @@ -44,11 +58,28 @@ namespace MediaBrowser.Model.Entities /// The comment. public string Comment { get; set; } + /// + /// Gets or sets the time base. + /// + /// The time base. public string TimeBase { get; set; } + + /// + /// Gets or sets the codec time base. + /// + /// The codec time base. public string CodecTimeBase { get; set; } + /// + /// Gets or sets the title. + /// + /// The title. public string Title { get; set; } + /// + /// Gets or sets the video range. + /// + /// The video range. public string VideoRange { get @@ -60,7 +91,8 @@ namespace MediaBrowser.Model.Entities var colorTransfer = ColorTransfer; - if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase) + || string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) { return "HDR"; } @@ -70,7 +102,9 @@ namespace MediaBrowser.Model.Entities } public string localizedUndefined { get; set; } + public string localizedDefault { get; set; } + public string localizedForced { get; set; } public string DisplayTitle @@ -197,34 +231,34 @@ namespace MediaBrowser.Model.Entities { if (i.IsInterlaced) { - return "1440I"; + return "1440i"; } - return "1440P"; + return "1440p"; } if (width >= 1900 || height >= 1000) { if (i.IsInterlaced) { - return "1080I"; + return "1080i"; } - return "1080P"; + return "1080p"; } if (width >= 1260 || height >= 700) { if (i.IsInterlaced) { - return "720I"; + return "720i"; } - return "720P"; + return "720p"; } if (width >= 700 || height >= 440) { if (i.IsInterlaced) { - return "480I"; + return "480i"; } - return "480P"; + return "480p"; } return "SD"; diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index b41d0af1d1..461f59672e 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -21,9 +21,9 @@ - + - + @@ -37,6 +37,9 @@ + + + ../jellyfin.ruleset diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index fe2fbe7e4f..b6d7b4245c 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -67,6 +67,7 @@ namespace MediaBrowser.Model.Net { ".m3u8", "application/x-mpegURL" }, { ".map", "application/x-javascript" }, { ".mobi", "application/x-mobipocket-ebook" }, + { ".opf", "application/oebps-package+xml" }, { ".pdf", "application/pdf" }, { ".rar", "application/vnd.rar" }, { ".srt", "application/x-subrip" }, @@ -99,6 +100,7 @@ namespace MediaBrowser.Model.Net { ".ssa", "text/x-ssa" }, { ".css", "text/css" }, { ".csv", "text/csv" }, + { ".edl", "text/plain" }, { ".rtf", "text/rtf" }, { ".txt", "text/plain" }, { ".vtt", "text/vtt" }, diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 1b3df63b63..5073b40157 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html index 34494644d4..fbf413f2b5 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html @@ -31,8 +31,8 @@ $('.configPage').on('pageshow', function () { Dashboard.showLoadingMsg(); ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - $('#enable').checked(config.Enable); - $('#replaceAlbumName').checked(config.ReplaceAlbumName); + $('#enable').checked = config.Enable; + $('#replaceAlbumName').checked = config.ReplaceAlbumName; Dashboard.hideLoadingMsg(); }); @@ -43,8 +43,8 @@ var form = this; ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - config.Enable = $('#enable', form).checked(); - config.ReplaceAlbumName = $('#replaceAlbumName', form).checked(); + config.Enable = $('#enable', form).checked; + config.ReplaceAlbumName = $('#replaceAlbumName', form).checked; ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html index 1f02461da2..90196b046b 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -41,8 +41,8 @@ ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { $('#server').val(config.Server).change(); $('#rateLimit').val(config.RateLimit).change(); - $('#enable').checked(config.Enable); - $('#replaceArtistName').checked(config.ReplaceArtistName); + $('#enable').checked = config.Enable; + $('#replaceArtistName').checked = config.ReplaceArtistName; Dashboard.hideLoadingMsg(); }); @@ -55,8 +55,8 @@ ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { config.Server = $('#server', form).val(); config.RateLimit = $('#rateLimit', form).val(); - config.Enable = $('#enable', form).checked(); - config.ReplaceArtistName = $('#replaceArtistName', form).checked(); + config.Enable = $('#enable', form).checked; + config.ReplaceArtistName = $('#replaceArtistName', form).checked; ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); }); diff --git a/MediaBrowser.Providers/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbSearch.cs index 223cef086b..bf63946084 100644 --- a/MediaBrowser.Providers/Tmdb/Movies/TmdbSearch.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbSearch.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using System.Text.RegularExpressions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -18,8 +19,22 @@ namespace MediaBrowser.Providers.Tmdb.Movies { public class TmdbSearch { - private static readonly CultureInfo EnUs = new CultureInfo("en-US"); - private const string Search3 = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}"; + private static readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + private static readonly Regex _cleanEnclosed = new Regex(@"\p{Ps}.*\p{Pe}", RegexOptions.Compiled); + private static readonly Regex _cleanNonWord = new Regex(@"[\W_]+", RegexOptions.Compiled); + private static readonly Regex _cleanStopWords = new Regex(@"\b( # Start at word boundary + 19[0-9]{2}|20[0-9]{2}| # 1900-2099 + S[0-9]{2}| # Season + E[0-9]{2}| # Episode + (2160|1080|720|576|480)[ip]?| # Resolution + [xh]?264| # Encoding + (web|dvd|bd|hdtv|hd)rip| # *Rip + web|hdtv|mp4|bluray|ktr|dl|single|imageset|internal|doku|dubbed|retail|xxx|flac + ).* # Match rest of string", + RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase); + + private const string _searchURL = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}"; private readonly ILogger _logger; private readonly IJsonSerializer _json; @@ -61,19 +76,18 @@ namespace MediaBrowser.Providers.Tmdb.Movies var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - if (!string.IsNullOrWhiteSpace(name)) - { - var parsedName = _libraryManager.ParseName(name); - var yearInName = parsedName.Year; - name = parsedName.Name; - year = year ?? yearInName; - } + // TODO: Investigate: Does this mean we are reparsing already parsed ItemLookupInfo? + var parsedName = _libraryManager.ParseName(name); + var yearInName = parsedName.Year; + name = parsedName.Name; + year ??= yearInName; - _logger.LogInformation("MovieDbProvider: Finding id for item: " + name); + _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name, year); var language = idInfo.MetadataLanguage.ToLowerInvariant(); - //nope - search for it - //var searchType = item is BoxSet ? "collection" : "movie"; + // Replace sequences of non-word characters with space + // TMDB expects a space separated list of words make sure that is the case + name = _cleanNonWord.Replace(name, " ").Trim(); var results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false); @@ -86,36 +100,35 @@ namespace MediaBrowser.Providers.Tmdb.Movies } } + // TODO: retrying alternatives should be done outside the search + // provider so that the retry logic can be common for all search + // providers if (results.Count == 0) { - // try with dot and _ turned to space - var originalName = name; + var name2 = parsedName.Name; - name = name.Replace(",", " "); - name = name.Replace(".", " "); - name = name.Replace("_", " "); - name = name.Replace("-", " "); - name = name.Replace("!", " "); - name = name.Replace("?", " "); + // Remove things enclosed in []{}() etc + name2 = _cleanEnclosed.Replace(name2, string.Empty); - var parenthIndex = name.IndexOf('('); - if (parenthIndex != -1) - { - name = name.Substring(0, parenthIndex); - } + // Replace sequences of non-word characters with space + name2 = _cleanNonWord.Replace(name2, " "); - name = name.Trim(); + // Clean based on common stop words / tokens + name2 = _cleanStopWords.Replace(name2, string.Empty); + + // Trim whitespace + name2 = name2.Trim(); // Search again if the new name is different - if (!string.Equals(name, originalName)) + if (!string.Equals(name2, name) && !string.IsNullOrWhiteSpace(name2)) { - results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false); + _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name2, year); + results = await GetSearchResults(name2, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false); if (results.Count == 0 && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) { //one more time, in english - results = await GetSearchResults(name, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false); - + results = await GetSearchResults(name2, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false); } } } @@ -150,7 +163,7 @@ namespace MediaBrowser.Providers.Tmdb.Movies throw new ArgumentException("name"); } - var url3 = string.Format(Search3, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, type); + var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, type); using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { @@ -179,14 +192,14 @@ namespace MediaBrowser.Providers.Tmdb.Movies if (!string.IsNullOrWhiteSpace(i.Release_Date)) { // These dates are always in this exact format - if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out var r)) + if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r)) { remoteResult.PremiereDate = r.ToUniversalTime(); remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; } } - remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(EnUs)); + remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(_usCulture)); return remoteResult; @@ -203,7 +216,7 @@ namespace MediaBrowser.Providers.Tmdb.Movies throw new ArgumentException("name"); } - var url3 = string.Format(Search3, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv"); + var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv"); using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { @@ -232,14 +245,14 @@ namespace MediaBrowser.Providers.Tmdb.Movies if (!string.IsNullOrWhiteSpace(i.First_Air_Date)) { // These dates are always in this exact format - if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out var r)) + if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r)) { remoteResult.PremiereDate = r.ToUniversalTime(); remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; } } - remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(EnUs)); + remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(_usCulture)); return remoteResult; diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 6b9fed83b3..e100c0b1cd 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -66,9 +66,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api.Tests", "tests\MediaBrowser.Api.Tests\MediaBrowser.Api.Tests.csproj", "{7C93C84F-105C-48E5-A878-406FA0A5B296}" EndProject Global @@ -185,10 +186,10 @@ Global {F03299F2-469F-40EF-A655-3766F97A5702}.Debug|Any CPU.Build.0 = Debug|Any CPU {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.ActiveCfg = Release|Any CPU {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.Build.0 = Release|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.Build.0 = Release|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.Build.0 = Debug|Any CPU {7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index fb76f34d0e..9c4b7b0b07 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -16,7 +16,7 @@ - + diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index ac0c970c13..8b14cf800d 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -21,5 +21,15 @@ + + + + + + + + + ../jellyfin-tests.ruleset + diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs index 40d80607c8..d11809de11 100644 --- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleParserTests.cs @@ -23,9 +23,9 @@ namespace Jellyfin.Naming.Tests.Subtitles var result = parser.ParseFile(input); - Assert.Equal(language, result.Language, true); - Assert.Equal(isDefault, result.IsDefault); - Assert.Equal(isForced, result.IsForced); + Assert.Equal(language, result?.Language, true); + Assert.Equal(isDefault, result?.IsDefault); + Assert.Equal(isForced, result?.IsForced); } [Theory] diff --git a/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs index 553d06681b..356ba216d6 100644 --- a/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/AbsoluteEpisodeNumberTests.cs @@ -21,7 +21,7 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(options) .Resolve(path, false, null, null, true); - Assert.Equal(episodeNumber, result.EpisodeNumber); + Assert.Equal(episodeNumber, result?.EpisodeNumber); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs index 6ecffe80b7..2937914b9f 100644 --- a/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/DailyEpisodeTests.cs @@ -6,8 +6,6 @@ namespace Jellyfin.Naming.Tests.TV { public class DailyEpisodeTests { - - [Theory] [InlineData(@"/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)] [InlineData(@"/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14)] @@ -23,12 +21,12 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(options) .Resolve(path, false); - Assert.Null(result.SeasonNumber); - Assert.Null(result.EpisodeNumber); - Assert.Equal(year, result.Year); - Assert.Equal(month, result.Month); - Assert.Equal(day, result.Day); - Assert.Equal(seriesName, result.SeriesName, true); + Assert.Null(result?.SeasonNumber); + Assert.Null(result?.EpisodeNumber); + Assert.Equal(year, result?.Year); + Assert.Equal(month, result?.Month); + Assert.Equal(day, result?.Day); + Assert.Equal(seriesName, result?.SeriesName, true); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs index 0c7d9520e2..8bd1a43d62 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs @@ -6,7 +6,6 @@ namespace Jellyfin.Naming.Tests.TV { public class EpisodeNumberWithoutSeasonTests { - [Theory] [InlineData(8, @"The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")] [InlineData(2, @"The Simpsons/The Simpsons - 02 - Ep Name.avi")] @@ -30,7 +29,7 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(options) .Resolve(path, false); - Assert.Equal(episodeNumber, result.EpisodeNumber); + Assert.Equal(episodeNumber, result?.EpisodeNumber); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs index 4b56067153..03aeb7f76b 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs @@ -1,4 +1,4 @@ -using Emby.Naming.Common; +using Emby.Naming.Common; using Emby.Naming.TV; using Xunit; @@ -35,7 +35,6 @@ namespace Jellyfin.Naming.Tests.TV // TODO: [InlineData("Watchmen (2019)/Watchmen 1x03 [WEBDL-720p][EAC3 5.1][h264][-TBS] - She Was Killed by Space Junk.mkv", "Watchmen (2019)", 1, 3)] // TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", "The Legend of Condor Heroes 2017", 1, 7)] public void ParseEpisodesCorrectly(string path, string name, int season, int episode) - { NamingOptions o = new NamingOptions(); EpisodePathParser p = new EpisodePathParser(o); diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs index 364eb7ff85..d0418a49ed 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeWithoutSeasonTests.cs @@ -19,9 +19,9 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(options) .Resolve(path, false); - Assert.Equal(seasonNumber, result.SeasonNumber); - Assert.Equal(episodeNumber, result.EpisodeNumber); - Assert.Equal(seriesName, result.SeriesName, true); + Assert.Equal(seasonNumber, result?.SeasonNumber); + Assert.Equal(episodeNumber, result?.EpisodeNumber); + Assert.Equal(seriesName, result?.SeriesName, ignoreCase: true); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs index 9eaf897b9e..4837e3a3b4 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs @@ -59,7 +59,7 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(_namingOptions) .Resolve(path, false); - Assert.Equal(expected, result.SeasonNumber); + Assert.Equal(expected, result?.SeasonNumber); } } } diff --git a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs index de253ce375..40b41b9f3d 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SimpleEpisodeTests.cs @@ -31,9 +31,9 @@ namespace Jellyfin.Naming.Tests.TV var result = new EpisodeResolver(options) .Resolve(path, false); - Assert.Equal(seasonNumber, result.SeasonNumber); - Assert.Equal(episodeNumber, result.EpisodeNumber); - Assert.Equal(seriesName, result.SeriesName, true); + Assert.Equal(seasonNumber, result?.SeasonNumber); + Assert.Equal(episodeNumber, result?.EpisodeNumber); + Assert.Equal(seriesName, result?.SeriesName, true); } } } diff --git a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs index d2b3d6ff0d..69de96a47a 100644 --- a/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/Format3DTests.cs @@ -25,8 +25,8 @@ namespace Jellyfin.Naming.Tests.Video var result = new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv"); - Assert.Equal("hsbs", result.Format3D); - Assert.Equal("Oblivion", result.Name); + Assert.Equal("hsbs", result?.Format3D); + Assert.Equal("Oblivion", result?.Name); } [Fact] diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 03fe32b6e1..4198d69ff8 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -12,7 +12,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiEdition1() + private void TestMultiEdition1() { var files = new[] { @@ -28,7 +28,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -37,7 +36,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiEdition2() + private void TestMultiEdition2() { var files = new[] { @@ -53,7 +52,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -76,7 +74,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -85,7 +82,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestLetterFolders() + private void TestLetterFolders() { var files = new[] { @@ -104,7 +101,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(7, result.Count); @@ -114,7 +110,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersionLimit() + private void TestMultiVersionLimit() { var files = new[] { @@ -134,7 +130,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -144,7 +139,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersionLimit2() + private void TestMultiVersionLimit2() { var files = new[] { @@ -165,7 +160,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(9, result.Count); @@ -175,7 +169,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion3() + private void TestMultiVersion3() { var files = new[] { @@ -192,7 +186,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(5, result.Count); @@ -202,7 +195,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion4() + private void TestMultiVersion4() { // Test for false positive @@ -221,7 +214,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(5, result.Count); @@ -231,7 +223,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion5() + private void TestMultiVersion5() { var files = new[] { @@ -251,7 +243,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -264,7 +255,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion6() + private void TestMultiVersion6() { var files = new[] { @@ -284,7 +275,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -297,7 +287,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion7() + private void TestMultiVersion7() { var files = new[] { @@ -311,7 +301,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(2, result.Count); @@ -319,7 +308,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion8() + private void TestMultiVersion8() { // This is not actually supported yet @@ -340,7 +329,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -353,7 +341,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion9() + private void TestMultiVersion9() { // Test for false positive @@ -372,7 +360,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(5, result.Count); @@ -382,7 +369,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion10() + private void TestMultiVersion10() { var files = new[] { @@ -396,7 +383,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -406,7 +392,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME // [Fact] - public void TestMultiVersion11() + private void TestMultiVersion11() { // Currently not supported but we should probably handle this. @@ -422,7 +408,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); diff --git a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs index 3630a07e46..8794d3ebe8 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StackTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StackTests.cs @@ -368,11 +368,11 @@ namespace Jellyfin.Naming.Tests.Video { var files = new[] { - new FileSystemMetadata{FullName = "Bad Boys (2006) part1.mkv", IsDirectory = false}, - new FileSystemMetadata{FullName = "Bad Boys (2006) part2.mkv", IsDirectory = false}, - new FileSystemMetadata{FullName = "300 (2006) part2", IsDirectory = true}, - new FileSystemMetadata{FullName = "300 (2006) part3", IsDirectory = true}, - new FileSystemMetadata{FullName = "300 (2006) part1", IsDirectory = true} + new FileSystemMetadata { FullName = "Bad Boys (2006) part1.mkv", IsDirectory = false }, + new FileSystemMetadata { FullName = "Bad Boys (2006) part2.mkv", IsDirectory = false }, + new FileSystemMetadata { FullName = "300 (2006) part2", IsDirectory = true }, + new FileSystemMetadata { FullName = "300 (2006) part3", IsDirectory = true }, + new FileSystemMetadata { FullName = "300 (2006) part1", IsDirectory = true } }; var resolver = GetResolver(); diff --git a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs index e31d97e2e3..30ba941365 100644 --- a/tests/Jellyfin.Naming.Tests/Video/StubTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/StubTests.cs @@ -31,10 +31,10 @@ namespace Jellyfin.Naming.Tests.Video var result = new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc"); - Assert.Equal("Oblivion", result.Name); + Assert.Equal("Oblivion", result?.Name); } - private void Test(string path, bool isStub, string stubType) + private void Test(string path, bool isStub, string? stubType) { var isStubResult = StubResolver.TryResolveFile(path, _namingOptions, out var stubTypeResult); diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs index 566dc9f7c1..12c4a50fe3 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs @@ -9,9 +9,10 @@ namespace Jellyfin.Naming.Tests.Video public class VideoListResolverTests { private readonly NamingOptions _namingOptions = new NamingOptions(); + // FIXME // [Fact] - public void TestStackAndExtras() + private void TestStackAndExtras() { // No stacking here because there is no part/disc/etc var files = new[] @@ -45,7 +46,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(5, result.Count); @@ -74,7 +74,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -95,7 +94,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -116,7 +114,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -138,7 +135,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -159,7 +155,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -184,7 +179,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(5, result.Count); @@ -205,7 +199,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = true, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -227,7 +220,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = true, FullName = i - }).ToList()).ToList(); Assert.Equal(2, result.Count); @@ -249,7 +241,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -271,7 +262,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -294,7 +284,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -317,7 +306,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(2, result.Count); @@ -337,7 +325,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -357,7 +344,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -378,7 +364,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -399,7 +384,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); @@ -422,7 +406,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Equal(4, result.Count); @@ -443,7 +426,6 @@ namespace Jellyfin.Naming.Tests.Video { IsDirectory = false, FullName = i - }).ToList()).ToList(); Assert.Single(result); diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs index 114735ceed..99828b2eb7 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs @@ -176,7 +176,6 @@ namespace Jellyfin.Naming.Tests.Video }; } - [Theory] [MemberData(nameof(GetResolveFileTestData))] public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult) @@ -184,17 +183,17 @@ namespace Jellyfin.Naming.Tests.Video var result = new VideoResolver(_namingOptions).ResolveFile(expectedResult.Path); Assert.NotNull(result); - Assert.Equal(result.Path, expectedResult.Path); - Assert.Equal(result.Container, expectedResult.Container); - Assert.Equal(result.Name, expectedResult.Name); - Assert.Equal(result.Year, expectedResult.Year); - Assert.Equal(result.ExtraType, expectedResult.ExtraType); - Assert.Equal(result.Format3D, expectedResult.Format3D); - Assert.Equal(result.Is3D, expectedResult.Is3D); - Assert.Equal(result.IsStub, expectedResult.IsStub); - Assert.Equal(result.StubType, expectedResult.StubType); - Assert.Equal(result.IsDirectory, expectedResult.IsDirectory); - Assert.Equal(result.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension); + Assert.Equal(result?.Path, expectedResult.Path); + Assert.Equal(result?.Container, expectedResult.Container); + Assert.Equal(result?.Name, expectedResult.Name); + Assert.Equal(result?.Year, expectedResult.Year); + Assert.Equal(result?.ExtraType, expectedResult.ExtraType); + Assert.Equal(result?.Format3D, expectedResult.Format3D); + Assert.Equal(result?.Is3D, expectedResult.Is3D); + Assert.Equal(result?.IsStub, expectedResult.IsStub); + Assert.Equal(result?.StubType, expectedResult.StubType); + Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory); + Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension); } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs new file mode 100644 index 0000000000..26dee38c6d --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -0,0 +1,21 @@ +using Emby.Server.Implementations.Library; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library +{ + public class IgnorePatternsTests + { + [Theory] + [InlineData("/media/small.jpg", true)] + [InlineData("/media/movies/#Recycle/test.txt", true)] + [InlineData("/media/movies/#recycle/", true)] + [InlineData("thumbs.db", true)] + [InlineData(@"C:\media\movies\movie.avi", false)] + [InlineData("/media/.hiddendir/file.mp4", true)] + [InlineData("/media/dir/.hiddenfile.mp4", true)] + public void PathIgnored(string path, bool expected) + { + Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); + } + } +} diff --git a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj index f30e486900..60c392314d 100644 --- a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj +++ b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj @@ -8,7 +8,7 @@ - +