Merge remote-tracking branch 'upstream/master' into syncplay

This commit is contained in:
gion 2020-05-26 10:23:09 +02:00
commit e4838b0faa
84 changed files with 1512 additions and 1303 deletions

View file

@ -2,7 +2,7 @@ ARG DOTNET_VERSION=3.1
FROM node:alpine as web-builder FROM node:alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master 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 - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \ && cd jellyfin-web-* \
&& yarn install \ && yarn install \

View file

@ -38,7 +38,7 @@ COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \ && 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 -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 [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 && \ echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
apt-get update && \ apt-get update && \

View file

@ -227,7 +227,7 @@ namespace Emby.Naming.Video
} }
return remainingFiles return remainingFiles
.Where(i => i.ExtraType == null) .Where(i => i.ExtraType != null)
.Where(i => baseNames.Any(b => .Where(i => baseNames.Any(b =>
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase))) i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
.ToList(); .ToList();

View file

@ -4,11 +4,10 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
@ -30,7 +29,7 @@ namespace Emby.Server.Implementations.Activity
/// </summary> /// </summary>
public sealed class ActivityLogEntryPoint : IServerEntryPoint public sealed class ActivityLogEntryPoint : IServerEntryPoint
{ {
private readonly ILogger _logger; private readonly ILogger<ActivityLogEntryPoint> _logger;
private readonly IInstallationManager _installationManager; private readonly IInstallationManager _installationManager;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
@ -38,14 +37,12 @@ namespace Emby.Server.Implementations.Activity
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly ISubtitleManager _subManager; private readonly ISubtitleManager _subManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IDeviceManager _deviceManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class. /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
/// </summary> /// </summary>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param> /// <param name="sessionManager">The session manager.</param>
/// <param name="deviceManager">The device manager.</param>
/// <param name="taskManager">The task manager.</param> /// <param name="taskManager">The task manager.</param>
/// <param name="activityManager">The activity manager.</param> /// <param name="activityManager">The activity manager.</param>
/// <param name="localization">The localization manager.</param> /// <param name="localization">The localization manager.</param>
@ -55,7 +52,6 @@ namespace Emby.Server.Implementations.Activity
public ActivityLogEntryPoint( public ActivityLogEntryPoint(
ILogger<ActivityLogEntryPoint> logger, ILogger<ActivityLogEntryPoint> logger,
ISessionManager sessionManager, ISessionManager sessionManager,
IDeviceManager deviceManager,
ITaskManager taskManager, ITaskManager taskManager,
IActivityManager activityManager, IActivityManager activityManager,
ILocalizationManager localization, ILocalizationManager localization,
@ -65,7 +61,6 @@ namespace Emby.Server.Implementations.Activity
{ {
_logger = logger; _logger = logger;
_sessionManager = sessionManager; _sessionManager = sessionManager;
_deviceManager = deviceManager;
_taskManager = taskManager; _taskManager = taskManager;
_activityManager = activityManager; _activityManager = activityManager;
_localization = localization; _localization = localization;
@ -99,52 +94,38 @@ namespace Emby.Server.Implementations.Activity
_userManager.UserPolicyUpdated += OnUserPolicyUpdated; _userManager.UserPolicyUpdated += OnUserPolicyUpdated;
_userManager.UserLockedOut += OnUserLockedOut; _userManager.UserLockedOut += OnUserLockedOut;
_deviceManager.CameraImageUploaded += OnCameraImageUploaded;
return Task.CompletedTask; return Task.CompletedTask;
} }
private void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e) private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{ {
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format( CultureInfo.InvariantCulture,
CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserLockedOutWithName"),
_localization.GetLocalizedString("CameraImageUploadedFrom"), e.Argument.Name),
e.Argument.Device.Name), NotificationType.UserLockedOut.ToString(),
Type = NotificationType.CameraImageUploaded.ToString() e.Argument.Id))
}); .ConfigureAwait(false);
} }
private void OnUserLockedOut(object sender, GenericEventArgs<User> e) private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
{ {
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
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(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
e.Provider, e.Provider,
Notifications.NotificationEntryPoint.GetItemName(e.Item)), Notifications.NotificationEntryPoint.GetItemName(e.Item)),
Type = "SubtitleDownloadFailure", "SubtitleDownloadFailure",
Guid.Empty)
{
ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture), ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
ShortOverview = e.Exception.Message 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; var item = e.MediaInfo;
@ -167,20 +148,19 @@ namespace Emby.Server.Implementations.Activity
var user = e.Users[0]; var user = e.Users[0];
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
user.Name, user.Name,
GetItemName(item), GetItemName(item),
e.DeviceName), e.DeviceName),
Type = GetPlaybackStoppedNotificationType(item.MediaType), GetPlaybackStoppedNotificationType(item.MediaType),
UserId = user.Id user.Id))
}); .ConfigureAwait(false);
} }
private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
{ {
var item = e.MediaInfo; var item = e.MediaInfo;
@ -203,17 +183,16 @@ namespace Emby.Server.Implementations.Activity
var user = e.Users.First(); var user = e.Users.First();
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStartedPlayingItemWithValues"), _localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
user.Name, user.Name,
GetItemName(item), GetItemName(item),
e.DeviceName), e.DeviceName),
Type = GetPlaybackNotificationType(item.MediaType), GetPlaybackNotificationType(item.MediaType),
UserId = user.Id user.Id))
}); .ConfigureAwait(false);
} }
private static string GetItemName(BaseItemDto item) private static string GetItemName(BaseItemDto item)
@ -263,7 +242,7 @@ namespace Emby.Server.Implementations.Activity
return null; return null;
} }
private void OnSessionEnded(object sender, SessionEventArgs e) private async void OnSessionEnded(object sender, SessionEventArgs e)
{ {
var session = e.SessionInfo; var session = e.SessionInfo;
@ -272,110 +251,108 @@ namespace Emby.Server.Implementations.Activity
return; return;
} }
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserOfflineFromDevice"), _localization.GetLocalizedString("UserOfflineFromDevice"),
session.UserName, session.UserName,
session.DeviceName), session.DeviceName),
Type = "SessionEnded", "SessionEnded",
session.UserId)
{
ShortOverview = string.Format( ShortOverview = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"), _localization.GetLocalizedString("LabelIpAddressValue"),
session.RemoteEndPoint), session.RemoteEndPoint),
UserId = session.UserId }).ConfigureAwait(false);
});
} }
private void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e) private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
{ {
var user = e.Argument.User; var user = e.Argument.User;
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("AuthenticationSucceededWithUserName"), _localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
user.Name), user.Name),
Type = "AuthenticationSucceeded", "AuthenticationSucceeded",
user.Id)
{
ShortOverview = string.Format( ShortOverview = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"), _localization.GetLocalizedString("LabelIpAddressValue"),
e.Argument.SessionInfo.RemoteEndPoint), e.Argument.SessionInfo.RemoteEndPoint),
UserId = user.Id }).ConfigureAwait(false);
});
} }
private void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e) private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
{ {
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("FailedLoginAttemptWithUserName"), _localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
e.Argument.Username), e.Argument.Username),
Type = "AuthenticationFailed", "AuthenticationFailed",
Guid.Empty)
{
LogSeverity = LogLevel.Error,
ShortOverview = string.Format( ShortOverview = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"), _localization.GetLocalizedString("LabelIpAddressValue"),
e.Argument.RemoteEndPoint), e.Argument.RemoteEndPoint),
Severity = LogLevel.Error }).ConfigureAwait(false);
});
} }
private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e) private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{ {
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserPolicyUpdatedWithName"), _localization.GetLocalizedString("UserPolicyUpdatedWithName"),
e.Argument.Name), e.Argument.Name),
Type = "UserPolicyUpdated", "UserPolicyUpdated",
UserId = e.Argument.Id e.Argument.Id))
}); .ConfigureAwait(false);
} }
private void OnUserDeleted(object sender, GenericEventArgs<User> e) private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{ {
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserDeletedWithName"), _localization.GetLocalizedString("UserDeletedWithName"),
e.Argument.Name), e.Argument.Name),
Type = "UserDeleted" "UserDeleted",
}); Guid.Empty))
.ConfigureAwait(false);
} }
private void OnUserPasswordChanged(object sender, GenericEventArgs<User> e) private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{ {
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserPasswordChangedWithName"), _localization.GetLocalizedString("UserPasswordChangedWithName"),
e.Argument.Name), e.Argument.Name),
Type = "UserPasswordChanged", "UserPasswordChanged",
UserId = e.Argument.Id e.Argument.Id))
}); .ConfigureAwait(false);
} }
private void OnUserCreated(object sender, GenericEventArgs<User> e) private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{ {
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserCreatedWithName"), _localization.GetLocalizedString("UserCreatedWithName"),
e.Argument.Name), e.Argument.Name),
Type = "UserCreated", "UserCreated",
UserId = e.Argument.Id e.Argument.Id))
}); .ConfigureAwait(false);
} }
private void OnSessionStarted(object sender, SessionEventArgs e) private async void OnSessionStarted(object sender, SessionEventArgs e)
{ {
var session = e.SessionInfo; var session = e.SessionInfo;
@ -384,87 +361,90 @@ namespace Emby.Server.Implementations.Activity
return; return;
} }
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserOnlineFromDevice"), _localization.GetLocalizedString("UserOnlineFromDevice"),
session.UserName, session.UserName,
session.DeviceName), session.DeviceName),
Type = "SessionStarted", "SessionStarted",
session.UserId)
{
ShortOverview = string.Format( ShortOverview = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"), _localization.GetLocalizedString("LabelIpAddressValue"),
session.RemoteEndPoint), session.RemoteEndPoint)
UserId = session.UserId }).ConfigureAwait(false);
});
} }
private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e) private async void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e)
{ {
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginUpdatedWithName"), _localization.GetLocalizedString("PluginUpdatedWithName"),
e.Argument.Item1.Name), e.Argument.Item1.Name),
Type = NotificationType.PluginUpdateInstalled.ToString(), NotificationType.PluginUpdateInstalled.ToString(),
Guid.Empty)
{
ShortOverview = string.Format( ShortOverview = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"), _localization.GetLocalizedString("VersionNumber"),
e.Argument.Item2.version), e.Argument.Item2.version),
Overview = e.Argument.Item2.changelog Overview = e.Argument.Item2.changelog
}); }).ConfigureAwait(false);
} }
private void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e) private async void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
{ {
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginUninstalledWithName"), _localization.GetLocalizedString("PluginUninstalledWithName"),
e.Argument.Name), e.Argument.Name),
Type = NotificationType.PluginUninstalled.ToString() NotificationType.PluginUninstalled.ToString(),
}); Guid.Empty))
.ConfigureAwait(false);
} }
private void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e) private async void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e)
{ {
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginInstalledWithName"), _localization.GetLocalizedString("PluginInstalledWithName"),
e.Argument.name), e.Argument.name),
Type = NotificationType.PluginInstalled.ToString(), NotificationType.PluginInstalled.ToString(),
Guid.Empty)
{
ShortOverview = string.Format( ShortOverview = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"), _localization.GetLocalizedString("VersionNumber"),
e.Argument.version) e.Argument.version)
}); }).ConfigureAwait(false);
} }
private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
{ {
var installationInfo = e.InstallationInfo; var installationInfo = e.InstallationInfo;
CreateLogEntry(new ActivityLogEntry await CreateLogEntry(new ActivityLog(
{ string.Format(
Name = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameInstallFailed"), _localization.GetLocalizedString("NameInstallFailed"),
installationInfo.Name), installationInfo.Name),
Type = NotificationType.InstallationFailed.ToString(), NotificationType.InstallationFailed.ToString(),
Guid.Empty)
{
ShortOverview = string.Format( ShortOverview = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"), _localization.GetLocalizedString("VersionNumber"),
installationInfo.Version), installationInfo.Version),
Overview = e.Exception.Message 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 result = e.Result;
var task = e.Task; var task = e.Task;
@ -495,22 +475,20 @@ namespace Emby.Server.Implementations.Activity
vals.Add(e.Result.LongErrorMessage); 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( LogSeverity = LogLevel.Error,
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("ScheduledTaskFailedWithName"),
task.Name),
Type = NotificationType.TaskFailed.ToString(),
Overview = string.Join(Environment.NewLine, vals), Overview = string.Join(Environment.NewLine, vals),
ShortOverview = runningTime, ShortOverview = runningTime
Severity = LogLevel.Error }).ConfigureAwait(false);
});
} }
} }
private void CreateLogEntry(ActivityLogEntry entry) private async Task CreateLogEntry(ActivityLog entry)
=> _activityManager.Create(entry); => await _activityManager.CreateAsync(entry).ConfigureAwait(false);
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
@ -537,8 +515,6 @@ namespace Emby.Server.Implementations.Activity
_userManager.UserDeleted -= OnUserDeleted; _userManager.UserDeleted -= OnUserDeleted;
_userManager.UserPolicyUpdated -= OnUserPolicyUpdated; _userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
_userManager.UserLockedOut -= OnUserLockedOut; _userManager.UserLockedOut -= OnUserLockedOut;
_deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
} }
/// <summary> /// <summary>
@ -566,7 +542,7 @@ namespace Emby.Server.Implementations.Activity
{ {
int months = days / DaysInMonth; int months = days / DaysInMonth;
values.Add(CreateValueString(months, "month")); values.Add(CreateValueString(months, "month"));
days %= DaysInMonth; days = days % DaysInMonth;
} }
// Number of days // Number of days

View file

@ -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
{
/// <summary>
/// The activity log manager.
/// </summary>
public class ActivityManager : IActivityManager
{
private readonly IActivityRepository _repo;
private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="ActivityManager"/> class.
/// </summary>
/// <param name="repo">The activity repository.</param>
/// <param name="userManager">The user manager.</param>
public ActivityManager(IActivityRepository repo, IUserManager userManager)
{
_repo = repo;
_userManager = userManager;
}
/// <inheritdoc />
public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
public void Create(ActivityLogEntry entry)
{
entry.Date = DateTime.UtcNow;
_repo.Create(entry);
EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(entry));
}
/// <inheritdoc />
public QueryResult<ActivityLogEntry> 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;
}
/// <inheritdoc />
public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit)
{
return GetActivityLogEntries(minDate, null, startIndex, limit);
}
}
}

View file

@ -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
{
/// <summary>
/// The activity log repository.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="ActivityRepository"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="appPaths">The server application paths.</param>
/// <param name="fileSystem">The filesystem.</param>
public ActivityRepository(ILogger<ActivityRepository> logger, IServerApplicationPaths appPaths, IFileSystem fileSystem)
: base(logger)
{
DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db");
_fileSystem = fileSystem;
}
/// <summary>
/// Initializes the <see cref="ActivityRepository"/>.
/// </summary>
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");
}
}
/// <inheritdoc />
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);
}
/// <summary>
/// Adds the provided <see cref="ActivityLogEntry"/> to this repository.
/// </summary>
/// <param name="entry">The activity log entry.</param>
/// <exception cref="ArgumentNullException">If entry is null.</exception>
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);
}
/// <inheritdoc />
public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit)
{
var commandText = BaseActivitySelectText;
var whereClauses = new List<string>();
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<ActivityLogEntry>();
var result = new QueryResult<ActivityLogEntry>();
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<IResultSetValue> 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<LogLevel>(reader[index].ToString(), true);
}
return info;
}
}
}

View file

@ -22,7 +22,6 @@ using Emby.Dlna.Ssdp;
using Emby.Drawing; using Emby.Drawing;
using Emby.Notifications; using Emby.Notifications;
using Emby.Photos; using Emby.Photos;
using Emby.Server.Implementations.Activity;
using Emby.Server.Implementations.Archiving; using Emby.Server.Implementations.Archiving;
using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Channels;
using Emby.Server.Implementations.Collections; using Emby.Server.Implementations.Collections;
@ -83,7 +82,6 @@ using MediaBrowser.Controller.TV;
using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
@ -632,9 +630,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
serviceCollection.AddSingleton<IActivityRepository, ActivityRepository>();
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>(); serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
serviceCollection.AddSingleton<ISessionContext, SessionContext>(); serviceCollection.AddSingleton<ISessionContext, SessionContext>();
@ -665,7 +660,6 @@ namespace Emby.Server.Implementations
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize(); ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
((SqliteUserRepository)Resolve<IUserRepository>()).Initialize(); ((SqliteUserRepository)Resolve<IUserRepository>()).Initialize();
((ActivityRepository)Resolve<IActivityRepository>()).Initialize();
SetStaticProperties(); SetStaticProperties();

View file

@ -193,12 +193,6 @@ namespace Emby.Server.Implementations.Configuration
changed = true; changed = true;
} }
if (!config.CameraUploadUpgraded)
{
config.CameraUploadUpgraded = true;
changed = true;
}
if (!config.CollectionsUpgraded) if (!config.CollectionsUpgraded)
{ {
config.CollectionsUpgraded = true; config.CollectionsUpgraded = true;

View file

@ -5,27 +5,18 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Devices; using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using MediaBrowser.Model.Users; using MediaBrowser.Model.Users;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Devices namespace Emby.Server.Implementations.Devices
{ {
@ -33,38 +24,23 @@ namespace Emby.Server.Implementations.Devices
{ {
private readonly IJsonSerializer _json; private readonly IJsonSerializer _json;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IFileSystem _fileSystem;
private readonly ILibraryMonitor _libraryMonitor;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localizationManager;
private readonly IAuthenticationRepository _authRepo; private readonly IAuthenticationRepository _authRepo;
private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache; private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
private readonly object _cameraUploadSyncLock = new object();
private readonly object _capabilitiesSyncLock = new object(); private readonly object _capabilitiesSyncLock = new object();
public DeviceManager( public DeviceManager(
IAuthenticationRepository authRepo, IAuthenticationRepository authRepo,
IJsonSerializer json, IJsonSerializer json,
ILibraryManager libraryManager,
ILocalizationManager localizationManager,
IUserManager userManager, IUserManager userManager,
IFileSystem fileSystem,
ILibraryMonitor libraryMonitor,
IServerConfigurationManager config) IServerConfigurationManager config)
{ {
_json = json; _json = json;
_userManager = userManager; _userManager = userManager;
_fileSystem = fileSystem;
_libraryMonitor = libraryMonitor;
_config = config; _config = config;
_libraryManager = libraryManager;
_localizationManager = localizationManager;
_authRepo = authRepo; _authRepo = authRepo;
_capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase); _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
} }
@ -194,172 +170,6 @@ namespace Emby.Server.Implementations.Devices
return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture)); 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<ContentUploadHistory>(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<CameraImageUploadInfo>
{
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<ContentUploadHistory>(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<Folder>()
.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<string, string, string> 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<string, string, string>(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) public bool CanAccessDevice(User user, string deviceId)
{ {
if (user == null) if (user == null)
@ -399,102 +209,4 @@ namespace Emby.Server.Implementations.Devices
return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase); 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<DeviceManagerEntryPoint> 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<ConfigurationStore> 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<DevicesOptions>("devices");
}
}
} }

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup> <PropertyGroup>
@ -34,15 +34,16 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" />
<PackageReference Include="Mono.Nat" Version="2.0.1" /> <PackageReference Include="Mono.Nat" Version="2.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" />
<PackageReference Include="sharpcompress" Version="0.25.0" /> <PackageReference Include="sharpcompress" Version="0.25.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.0.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -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) if (ignoreStackTrace)
{ {
_logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog); _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 requestInnerEx = GetActualException(requestEx);
var statusCode = GetStatusCode(requestInnerEx); var statusCode = GetStatusCode(requestInnerEx);
// Do not handle 500 server exceptions manually when in development mode foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
// The framework-defined development exception page will be returned instead {
if (statusCode == 500 && _hostEnvironment.IsDevelopment()) 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; throw;
} }
await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog).ConfigureAwait(false); await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
} }
catch (Exception handlerException) catch (Exception handlerException)
{ {

View file

@ -11,6 +11,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Emby.Server.Implementations.Library;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO namespace Emby.Server.Implementations.IO
@ -37,38 +38,6 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Any file name ending in any of these will be ignored by the watchers.
/// </summary>
private static readonly HashSet<string> _alwaysIgnoreFiles = new HashSet<string>(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<string> _alwaysIgnoreExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
// thumbs.db
".db",
// bts sync files
".bts",
".sync"
};
/// <summary> /// <summary>
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
/// </summary> /// </summary>
@ -395,12 +364,7 @@ namespace Emby.Server.Implementations.IO
throw new ArgumentNullException(nameof(path)); throw new ArgumentNullException(nameof(path));
} }
var filename = Path.GetFileName(path); var monitorPath = !IgnorePatterns.ShouldIgnore(path);
var monitorPath = !string.IsNullOrEmpty(filename) &&
!_alwaysIgnoreFiles.Contains(filename) &&
!_alwaysIgnoreExtensions.Contains(Path.GetExtension(path)) &&
_alwaysIgnoreSubstrings.All(i => path.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1);
// Ignore certain files // Ignore certain files
var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList(); var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList();

View file

@ -1,7 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
@ -16,32 +14,6 @@ namespace Emby.Server.Implementations.Library
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
/// <summary>
/// Any folder named in this list will be ignored
/// </summary>
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",
};
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class. /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
/// </summary> /// </summary>
@ -60,23 +32,15 @@ namespace Emby.Server.Implementations.Library
return false; return false;
} }
var filename = fileInfo.Name; if (IgnorePatterns.ShouldIgnore(fileInfo.FullName))
// Ignore hidden files on UNIX
if (Environment.OSVersion.Platform != PlatformID.Win32NT
&& filename[0] == '.')
{ {
return true; return true;
} }
var filename = fileInfo.Name;
if (fileInfo.IsDirectory) if (fileInfo.IsDirectory)
{ {
// Ignore any folders in our list
if (_ignoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase))
{
return true;
}
if (parent != null) if (parent != null)
{ {
// Ignore trailer folders but allow it at the collection level // Ignore trailer folders but allow it at the collection level
@ -109,11 +73,6 @@ namespace Emby.Server.Implementations.Library
return true; return true;
} }
} }
// Ignore samples
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
return m.Success;
} }
return false; return false;

View file

@ -0,0 +1,74 @@
using System.Linq;
using DotNet.Globbing;
namespace Emby.Server.Implementations.Library
{
/// <summary>
/// Glob patterns for files to ignore
/// </summary>
public static class IgnorePatterns
{
/// <summary>
/// Files matching these glob patterns will be ignored
/// </summary>
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();
/// <summary>
/// Returns true if the supplied path should be ignored
/// </summary>
public static bool ShouldIgnore(string path)
{
return _globs.Any(g => g.IsMatch(path));
}
}
}

View file

@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{ {
public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book> public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
{ {
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) protected override Book Resolve(ItemResolveArgs args)
{ {

View file

@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
@ -118,6 +119,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
//OpenedMediaSource.SupportsDirectStream = true; //OpenedMediaSource.SupportsDirectStream = true;
//OpenedMediaSource.SupportsTranscoding = true; //OpenedMediaSource.SupportsTranscoding = true;
await taskCompletionSource.Task.ConfigureAwait(false); 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<bool> openTaskCompletionSource, CancellationToken cancellationToken) private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
@ -139,14 +151,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
cancellationToken).ConfigureAwait(false); 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) 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; EnableStreamSharing = false;
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false); await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
}); });

View file

@ -9,7 +9,7 @@
"Channels": "القنوات", "Channels": "القنوات",
"ChapterNameValue": "الفصل {0}", "ChapterNameValue": "الفصل {0}",
"Collections": "مجموعات", "Collections": "مجموعات",
"DeviceOfflineWithName": "قُطِع الاتصال بـ{0}", "DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل", "DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}", "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
"Favorites": "المفضلة", "Favorites": "المفضلة",

View file

@ -23,7 +23,7 @@
"HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteEpisodes": "Oblíbené epizody",
"HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteShows": "Oblíbené seriály",
"HeaderFavoriteSongs": "Oblíbená hudba", "HeaderFavoriteSongs": "Oblíbená hudba",
"HeaderLiveTV": "Živá TV", "HeaderLiveTV": "Televize",
"HeaderNextUp": "Nadcházející", "HeaderNextUp": "Nadcházející",
"HeaderRecordingGroups": "Skupiny nahrávek", "HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domáci videa", "HomeVideos": "Domáci videa",

View file

@ -24,7 +24,7 @@
"HeaderFavoriteShows": "Programas favoritos", "HeaderFavoriteShows": "Programas favoritos",
"HeaderFavoriteSongs": "Canciones favoritas", "HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en vivo", "HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "A Continuación", "HeaderNextUp": "Siguiente",
"HeaderRecordingGroups": "Grupos de grabación", "HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Videos caseros", "HomeVideos": "Videos caseros",
"Inherit": "Heredar", "Inherit": "Heredar",
@ -44,7 +44,7 @@
"NameInstallFailed": "{0} instalación fallida", "NameInstallFailed": "{0} instalación fallida",
"NameSeasonNumber": "Temporada {0}", "NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconocida", "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", "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada", "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada",
"NotificationOptionAudioPlayback": "Se inició la reproducción de audio", "NotificationOptionAudioPlayback": "Se inició la reproducción de audio",
@ -56,7 +56,7 @@
"NotificationOptionPluginInstalled": "Complemento instalado", "NotificationOptionPluginInstalled": "Complemento instalado",
"NotificationOptionPluginUninstalled": "Complemento desinstalado", "NotificationOptionPluginUninstalled": "Complemento desinstalado",
"NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada", "NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada",
"NotificationOptionServerRestartRequired": "Se necesita reiniciar el Servidor", "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
"NotificationOptionTaskFailed": "Falla de tarea programada", "NotificationOptionTaskFailed": "Falla de tarea programada",
"NotificationOptionUserLockedOut": "Usuario bloqueado", "NotificationOptionUserLockedOut": "Usuario bloqueado",
"NotificationOptionVideoPlayback": "Se inició la reproducción de video", "NotificationOptionVideoPlayback": "Se inició la reproducción de video",
@ -71,7 +71,7 @@
"ScheduledTaskFailedWithName": "{0} falló", "ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciado", "ScheduledTaskStartedWithName": "{0} iniciado",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Series", "Shows": "Programas",
"Songs": "Canciones", "Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.", "StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
@ -94,25 +94,25 @@
"ValueSpecialEpisodeName": "Especial - {0}", "ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versión {0}", "VersionNumber": "Versión {0}",
"TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten basándose en la configuración de los metadatos.", "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.", "TaskRefreshChannelsDescription": "Actualizar información de canales de internet.",
"TaskRefreshChannels": "Actualizar canales", "TaskRefreshChannels": "Actualizar canales",
"TaskCleanTranscodeDescription": "Eliminar archivos transcodificados con mas de un día de antigüedad.", "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.", "TaskUpdatePluginsDescription": "Descargar e instalar actualizaciones para complementos que estén configurados en actualizar automáticamente.",
"TaskUpdatePlugins": "Actualizar complementos", "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", "TaskRefreshPeople": "Actualizar personas",
"TaskCleanLogsDescription": "Eliminar archivos de registro que tengan mas de {0} días de antigüedad.", "TaskCleanLogsDescription": "Eliminar archivos de registro que tengan mas de {0} días de antigüedad.",
"TaskCleanLogs": "Limpiar directorio de registros", "TaskCleanLogs": "Limpiar directorio de registros",
"TaskRefreshLibraryDescription": "Escanear su librería multimedia por nuevos archivos y refrescar metadatos.", "TaskRefreshLibraryDescription": "Escanear su biblioteca multimedia por nuevos archivos y refrescar metadatos.",
"TaskRefreshLibrary": "Escanear librería multimedia", "TaskRefreshLibrary": "Escanear biblioteca multimedia",
"TaskRefreshChapterImagesDescription": "Crear miniaturas de videos que tengan capítulos.", "TaskRefreshChapterImagesDescription": "Crear miniaturas de videos que tengan capítulos.",
"TaskRefreshChapterImages": "Extraer imágenes de capitulo", "TaskRefreshChapterImages": "Extraer imágenes de capítulo",
"TaskCleanCacheDescription": "Eliminar archivos de cache que no se necesiten en el sistema.", "TaskCleanCacheDescription": "Eliminar archivos de caché que no se necesiten en el sistema.",
"TaskCleanCache": "Limpiar directorio Cache", "TaskCleanCache": "Limpiar directorio caché",
"TasksChannelsCategory": "Canales de Internet", "TasksChannelsCategory": "Canales de internet",
"TasksApplicationCategory": "Solicitud", "TasksApplicationCategory": "Aplicación",
"TasksLibraryCategory": "Biblioteca", "TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Mantenimiento" "TasksMaintenanceCategory": "Mantenimiento"
} }

View file

@ -16,16 +16,16 @@
"Folders": "Carpetas", "Folders": "Carpetas",
"Genres": "Géneros", "Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum", "HeaderAlbumArtists": "Artistas del álbum",
"HeaderCameraUploads": "Subidos desde Camara", "HeaderCameraUploads": "Subidas desde la cámara",
"HeaderContinueWatching": "Continuar Viendo", "HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos", "HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Programas favoritos", "HeaderFavoriteShows": "Programas favoritos",
"HeaderFavoriteSongs": "Canciones favoritas", "HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en Vivo", "HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "A Continuación", "HeaderNextUp": "A continuación",
"HeaderRecordingGroups": "Grupos de Grabaciones", "HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Videos caseros", "HomeVideos": "Videos caseros",
"Inherit": "Heredar", "Inherit": "Heredar",
"ItemAddedWithName": "{0} fue agregado a la biblioteca", "ItemAddedWithName": "{0} fue agregado a la biblioteca",
@ -41,12 +41,12 @@
"Movies": "Películas", "Movies": "Películas",
"Music": "Música", "Music": "Música",
"MusicVideos": "Videos musicales", "MusicVideos": "Videos musicales",
"NameInstallFailed": "{0} instalación fallida", "NameInstallFailed": "Falló la instalación de {0}",
"NameSeasonNumber": "Temporada {0}", "NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconocida", "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 aplicación disponible", "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualización de aplicación instalada", "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada",
"NotificationOptionAudioPlayback": "Reproducción de audio iniciada", "NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida", "NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
"NotificationOptionCameraImageUploaded": "Imagen de la cámara subida", "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
@ -56,7 +56,7 @@
"NotificationOptionPluginInstalled": "Complemento instalado", "NotificationOptionPluginInstalled": "Complemento instalado",
"NotificationOptionPluginUninstalled": "Complemento desinstalado", "NotificationOptionPluginUninstalled": "Complemento desinstalado",
"NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada", "NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada",
"NotificationOptionServerRestartRequired": "Se necesita reiniciar el Servidor", "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
"NotificationOptionTaskFailed": "Falla de tarea programada", "NotificationOptionTaskFailed": "Falla de tarea programada",
"NotificationOptionUserLockedOut": "Usuario bloqueado", "NotificationOptionUserLockedOut": "Usuario bloqueado",
"NotificationOptionVideoPlayback": "Reproducción de video iniciada", "NotificationOptionVideoPlayback": "Reproducción de video iniciada",
@ -69,48 +69,48 @@
"PluginUpdatedWithName": "{0} fue actualizado", "PluginUpdatedWithName": "{0} fue actualizado",
"ProviderValue": "Proveedor: {0}", "ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló", "ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} Iniciado", "ScheduledTaskStartedWithName": "{0} iniciado",
"ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado", "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
"Shows": "Programas", "Shows": "Programas",
"Songs": "Canciones", "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}", "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", "Sync": "Sincronizar",
"System": "Sistema", "System": "Sistema",
"TvShows": "Programas de TV", "TvShows": "Programas de TV",
"User": "Usuario", "User": "Usuario",
"UserCreatedWithName": "Se ha creado el usuario {0}", "UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "Se ha eliminado el usuario {0}", "UserDeletedWithName": "El usuario {0} ha sido eliminado",
"UserDownloadingItemWithValues": "{0} esta descargando {1}", "UserDownloadingItemWithValues": "{0} está descargando {1}",
"UserLockedOutWithName": "El usuario {0} ha sido bloqueado", "UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}", "UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
"UserOnlineFromDevice": "{0} está en línea desde {1}", "UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}", "UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
"UserPolicyUpdatedWithName": "Las política de usuario ha sido actualizada por {0}", "UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
"UserStartedPlayingItemWithValues": "{0} está reproduciéndose {1} en {2}", "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducirse {1} en {2}", "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
"ValueHasBeenAddedToLibrary": "{0} se han añadido a su biblioteca de medios", "ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca de medios",
"ValueSpecialEpisodeName": "Especial - {0}", "ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versión {0}", "VersionNumber": "Versión {0}",
"TaskDownloadMissingSubtitlesDescription": "Buscar subtítulos de internet basado en configuración de metadatos.", "TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
"TaskDownloadMissingSubtitles": "Descargar subtítulos perdidos", "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
"TaskRefreshChannelsDescription": "Refrescar información de canales de internet.", "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
"TaskRefreshChannels": "Actualizar canales", "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", "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", "TaskUpdatePlugins": "Actualizar complementos",
"TaskRefreshPeopleDescription": "Actualizar datos de actores y directores en su librería multimedia.", "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
"TaskRefreshPeople": "Refrescar persona", "TaskRefreshPeople": "Actualizar personas",
"TaskCleanLogsDescription": "Eliminar archivos de registro con mas de {0} días.", "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad.",
"TaskCleanLogs": "Directorio de logo limpio", "TaskCleanLogs": "Limpiar directorio de registros",
"TaskRefreshLibraryDescription": "Escanear su librería multimedia para nuevos archivos y refrescar metadatos.", "TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios por archivos nuevos y actualiza los metadatos.",
"TaskRefreshLibrary": "Escanear librería multimerdia", "TaskRefreshLibrary": "Escanear biblioteca de medios",
"TaskRefreshChapterImagesDescription": "Crear miniaturas para videos con capítulos.", "TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
"TaskRefreshChapterImages": "Extraer imágenes de capítulos", "TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
"TaskCleanCacheDescription": "Eliminar archivos cache que ya no se necesiten por el sistema.", "TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.",
"TaskCleanCache": "Limpiar directorio cache", "TaskCleanCache": "Limpiar directorio caché",
"TasksChannelsCategory": "Canales de Internet", "TasksChannelsCategory": "Canales de Internet",
"TasksApplicationCategory": "Aplicación", "TasksApplicationCategory": "Aplicación",
"TasksLibraryCategory": "Biblioteca", "TasksLibraryCategory": "Biblioteca",

View file

@ -96,21 +96,22 @@
"TasksLibraryCategory": "Bibliothèque", "TasksLibraryCategory": "Bibliothèque",
"TasksMaintenanceCategory": "Entretien", "TasksMaintenanceCategory": "Entretien",
"TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.", "TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.",
"TaskDownloadMissingSubtitles": "Télécharger des sous-titres manquants", "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants",
"TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines d'internet.", "TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines internet.",
"TaskRefreshChannels": "Rafraîchir des chaines", "TaskRefreshChannels": "Rafraîchir des chaines",
"TaskCleanTranscodeDescription": "Retirer des fichiers de transcodage de plus qu'un jour.", "TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage de plus d'un jour.",
"TaskCleanTranscode": "Nettoyer le directoire de transcodage", "TaskCleanTranscode": "Nettoyer le répertoire de transcodage",
"TaskUpdatePluginsDescription": "Télécharger et installer des mises à jours des plugins qui sont configurés m.à.j. automisés.", "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 plugins", "TaskUpdatePlugins": "Mise à jour des extensions",
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.", "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", "TaskRefreshPeople": "Rafraîchir les acteurs",
"TaskCleanLogsDescription": "Retire les données qui ont plus que {0} jours.", "TaskCleanLogsDescription": "Supprime les journaux qui ont plus que {0} jours.",
"TaskCleanLogs": "Nettoyer les données de directoire", "TaskCleanLogs": "Nettoyer le répertoire des journaux",
"TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour des nouveaux fichiers et rafraîchit les métadonnées.", "TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour trouver de nouveaux fichiers et rafraîchit les métadonnées.",
"TaskRefreshChapterImages": "Extraire des images du chapitre", "TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskRefreshChapterImagesDescription": "Créer des vignettes pour des vidéos qui ont des chapitres", "TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres",
"TaskRefreshLibrary": "Analyser la bibliothèque de média", "TaskRefreshLibrary": "Analyser la bibliothèque de médias",
"TaskCleanCache": "Nettoyer le cache de directoire", "TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires",
"TasksApplicationCategory": "Application" "TasksApplicationCategory": "Application",
"TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système."
} }

View file

@ -107,5 +107,12 @@
"TaskCleanLogs": "נקה תיקיית יומן", "TaskCleanLogs": "נקה תיקיית יומן",
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.", "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.", "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
"TasksChannelsCategory": "ערוצי אינטרנט" "TasksChannelsCategory": "ערוצי אינטרנט",
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.",
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
"TaskRefreshChannels": "רענן ערוץ",
"TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",
"TaskCleanTranscode": "נקה תקיית Transcode",
"TaskUpdatePluginsDescription": "הורד והתקן עדכונים עבור תוספים שמוגדרים לעדכון אוטומטי."
} }

View file

@ -80,16 +80,32 @@
"ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt", "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}", "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
"UserStartedPlayingItemWithValues": "{0} er að spila {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", "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}", "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
"UserOfflineFromDevice": "{0} hefur aftengst frá {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}", "UserDownloadingItemWithValues": "{0} Hleður niður {1}",
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}", "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
"ProviderValue": "Veitandi: {0}", "ProviderValue": "Veitandi: {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón", "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
"ValueSpecialEpisodeName": "Sérstakt - {0}", "ValueSpecialEpisodeName": "Sérstakt - {0}",
"Shows": "Þættir", "Shows": "Sýningar",
"Playlists": "Spilunarlisti" "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"
} }

View file

@ -92,5 +92,27 @@
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}", "UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
"ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką", "ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką",
"ValueSpecialEpisodeName": "Ypatinga - {0}", "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"
} }

View file

@ -19,10 +19,10 @@
"HeaderCameraUploads": "Envios da Câmera", "HeaderCameraUploads": "Envios da Câmera",
"HeaderContinueWatching": "Continuar Assistindo", "HeaderContinueWatching": "Continuar Assistindo",
"HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos", "HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episódios Favoritos", "HeaderFavoriteEpisodes": "Episódios favoritos",
"HeaderFavoriteShows": "Séries Favoritas", "HeaderFavoriteShows": "Séries favoritas",
"HeaderFavoriteSongs": "Músicas Favoritas", "HeaderFavoriteSongs": "Músicas favoritas",
"HeaderLiveTV": "TV ao Vivo", "HeaderLiveTV": "TV ao Vivo",
"HeaderNextUp": "A Seguir", "HeaderNextUp": "A Seguir",
"HeaderRecordingGroups": "Grupos de Gravação", "HeaderRecordingGroups": "Grupos de Gravação",

View file

@ -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": "อัลบั้ม"
}

View file

@ -11,15 +11,15 @@
"Collections": "合輯", "Collections": "合輯",
"DeviceOfflineWithName": "{0} 已經斷開連結", "DeviceOfflineWithName": "{0} 已經斷開連結",
"DeviceOnlineWithName": "{0} 已經連接", "DeviceOnlineWithName": "{0} 已經連接",
"FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試", "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗",
"Favorites": "我的最愛", "Favorites": "我的最愛",
"Folders": "檔案夾", "Folders": "檔案夾",
"Genres": "風格", "Genres": "風格",
"HeaderAlbumArtists": "專輯藝術家", "HeaderAlbumArtists": "專輯藝",
"HeaderCameraUploads": "相機上載", "HeaderCameraUploads": "相機上載",
"HeaderContinueWatching": "繼續觀看", "HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛專輯", "HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛藝術家", "HeaderFavoriteArtists": "最愛的藝人",
"HeaderFavoriteEpisodes": "最愛的劇集", "HeaderFavoriteEpisodes": "最愛的劇集",
"HeaderFavoriteShows": "最愛的節目", "HeaderFavoriteShows": "最愛的節目",
"HeaderFavoriteSongs": "最愛的歌曲", "HeaderFavoriteSongs": "最愛的歌曲",
@ -33,14 +33,14 @@
"LabelIpAddressValue": "IP 地址: {0}", "LabelIpAddressValue": "IP 地址: {0}",
"LabelRunningTimeValue": "運行時間: {0}", "LabelRunningTimeValue": "運行時間: {0}",
"Latest": "最新", "Latest": "最新",
"MessageApplicationUpdated": "Jellyfin Server 已更新", "MessageApplicationUpdated": "Jellyfin 伺服器已更新",
"MessageApplicationUpdatedTo": "Jellyfin 伺服器已更新至 {0}", "MessageApplicationUpdatedTo": "Jellyfin 伺服器已更新至 {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已更新", "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已更新",
"MessageServerConfigurationUpdated": "伺服器設定已經更新", "MessageServerConfigurationUpdated": "伺服器設定已經更新",
"MixedContent": "Mixed content", "MixedContent": "混合內容",
"Movies": "電影", "Movies": "電影",
"Music": "音樂", "Music": "音樂",
"MusicVideos": "音樂MV", "MusicVideos": "音樂視頻",
"NameInstallFailed": "{0} 安裝失敗", "NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季", "NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數", "NameSeasonUnknown": "未知季數",
@ -49,7 +49,7 @@
"NotificationOptionApplicationUpdateInstalled": "應用程式已更新", "NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
"NotificationOptionAudioPlayback": "開始播放音頻", "NotificationOptionAudioPlayback": "開始播放音頻",
"NotificationOptionAudioPlaybackStopped": "已停止播放音頻", "NotificationOptionAudioPlaybackStopped": "已停止播放音頻",
"NotificationOptionCameraImageUploaded": "相機相片已上傳", "NotificationOptionCameraImageUploaded": "相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已添加新内容", "NotificationOptionNewLibraryContent": "已添加新内容",
"NotificationOptionPluginError": "擴充元件錯誤", "NotificationOptionPluginError": "擴充元件錯誤",
@ -63,11 +63,11 @@
"NotificationOptionVideoPlaybackStopped": "已停止播放視頻", "NotificationOptionVideoPlaybackStopped": "已停止播放視頻",
"Photos": "相片", "Photos": "相片",
"Playlists": "播放清單", "Playlists": "播放清單",
"Plugin": "Plugin", "Plugin": "插件",
"PluginInstalledWithName": "已安裝 {0}", "PluginInstalledWithName": "已安裝 {0}",
"PluginUninstalledWithName": "已移除 {0}", "PluginUninstalledWithName": "已移除 {0}",
"PluginUpdatedWithName": "已更新 {0}", "PluginUpdatedWithName": "已更新 {0}",
"ProviderValue": "Provider: {0}", "ProviderValue": "提供者: {0}",
"ScheduledTaskFailedWithName": "{0} 任務失敗", "ScheduledTaskFailedWithName": "{0} 任務失敗",
"ScheduledTaskStartedWithName": "{0} 任務開始", "ScheduledTaskStartedWithName": "{0} 任務開始",
"ServerNameNeedsToBeRestarted": "{0} 需要重啓", "ServerNameNeedsToBeRestarted": "{0} 需要重啓",
@ -77,17 +77,17 @@
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕", "SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕",
"Sync": "同步", "Sync": "同步",
"System": "System", "System": "系統",
"TvShows": "電視節目", "TvShows": "電視節目",
"User": "User", "User": "使用者",
"UserCreatedWithName": "用家 {0} 已創建", "UserCreatedWithName": "使用者 {0} 已創建",
"UserDeletedWithName": "用家 {0} 已移除", "UserDeletedWithName": "使用者 {0} 已移除",
"UserDownloadingItemWithValues": "{0} 正在下載 {1}", "UserDownloadingItemWithValues": "{0} 正在下載 {1}",
"UserLockedOutWithName": "用家 {0} 已被鎖定", "UserLockedOutWithName": "使用者 {0} 已被鎖定",
"UserOfflineFromDevice": "{0} 已從 {1} 斷開", "UserOfflineFromDevice": "{0} 已從 {1} 斷開",
"UserOnlineFromDevice": "{0} 已連綫,來自 {1}", "UserOnlineFromDevice": "{0} 已連綫,來自 {1}",
"UserPasswordChangedWithName": "用家 {0} 的密碼已變更", "UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
"UserPolicyUpdatedWithName": "用戶協議已被更新為 {0}", "UserPolicyUpdatedWithName": "使用者協議已更新為 {0}",
"UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}", "UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}",
"UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}", "UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}",
"ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫", "ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫",
@ -95,5 +95,23 @@
"VersionNumber": "版本{0}", "VersionNumber": "版本{0}",
"TaskDownloadMissingSubtitles": "下載遺失的字幕", "TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskUpdatePlugins": "更新插件", "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": "庫"
} }

View file

@ -1,3 +1,4 @@
using System.Security.Authentication;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -59,6 +60,10 @@ namespace Jellyfin.Api.Auth
return Task.FromResult(AuthenticateResult.Success(ticket)); return Task.FromResult(AuthenticateResult.Success(ticket));
} }
catch (AuthenticationException ex)
{
return Task.FromResult(AuthenticateResult.Fail(ex));
}
catch (SecurityException ex) catch (SecurityException ex)
{ {
return Task.FromResult(AuthenticateResult.Fail(ex)); return Task.FromResult(AuthenticateResult.Fail(ex));

View file

@ -13,7 +13,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.3" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" />
</ItemGroup> </ItemGroup>

View file

@ -0,0 +1,154 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Data.Entities
{
/// <summary>
/// An entity referencing an activity log entry.
/// </summary>
public partial class ActivityLog : ISavingChanges
{
/// <summary>
/// Initializes a new instance of the <see cref="ActivityLog"/> class.
/// Public constructor with required data.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="type">The type.</param>
/// <param name="userId">The user id.</param>
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();
}
/// <summary>
/// Initializes a new instance of the <see cref="ActivityLog"/> class.
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
protected ActivityLog()
{
Init();
}
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// </summary>
/// <param name="name">The name.</param>
/// <param name="type">The type.</param>
/// <param name="userId">The user's id.</param>
/// <returns>The new <see cref="ActivityLog"/> instance.</returns>
public static ActivityLog Create(string name, string type, Guid userId)
{
return new ActivityLog(name, type, userId);
}
/*************************************************************************
* Properties
*************************************************************************/
/// <summary>
/// Gets or sets the identity of this instance.
/// This is the key in the backing database.
/// </summary>
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
/// <summary>
/// Gets or sets the name.
/// Required, Max length = 512.
/// </summary>
[Required]
[MaxLength(512)]
[StringLength(512)]
public string Name { get; set; }
/// <summary>
/// Gets or sets the overview.
/// Max length = 512.
/// </summary>
[MaxLength(512)]
[StringLength(512)]
public string Overview { get; set; }
/// <summary>
/// Gets or sets the short overview.
/// Max length = 512.
/// </summary>
[MaxLength(512)]
[StringLength(512)]
public string ShortOverview { get; set; }
/// <summary>
/// Gets or sets the type.
/// Required, Max length = 256.
/// </summary>
[Required]
[MaxLength(256)]
[StringLength(256)]
public string Type { get; set; }
/// <summary>
/// Gets or sets the user id.
/// Required.
/// </summary>
[Required]
public Guid UserId { get; set; }
/// <summary>
/// Gets or sets the item id.
/// Max length = 256.
/// </summary>
[MaxLength(256)]
[StringLength(256)]
public string ItemId { get; set; }
/// <summary>
/// Gets or sets the date created. This should be in UTC.
/// Required.
/// </summary>
[Required]
public DateTime DateCreated { get; set; }
/// <summary>
/// Gets or sets the log severity. Default is <see cref="LogLevel.Trace"/>.
/// Required.
/// </summary>
[Required]
public LogLevel LogSeverity { get; set; }
/// <summary>
/// Gets or sets the row version.
/// Required, ConcurrencyToken.
/// </summary>
[ConcurrencyCheck]
[Required]
public uint RowVersion { get; set; }
partial void Init();
/// <inheritdoc />
public void OnSavingChanges()
{
RowVersion++;
}
}
}

View file

@ -1,12 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="2.2.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -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
{
/// <summary>
/// Manages the storage and retrieval of <see cref="ActivityLog"/> instances.
/// </summary>
public class ActivityManager : IActivityManager
{
private readonly JellyfinDbProvider _provider;
/// <summary>
/// Initializes a new instance of the <see cref="ActivityManager"/> class.
/// </summary>
/// <param name="provider">The Jellyfin database provider.</param>
public ActivityManager(JellyfinDbProvider provider)
{
_provider = provider;
}
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
/// <inheritdoc/>
public void Create(ActivityLog entry)
{
using var dbContext = _provider.CreateContext();
dbContext.ActivityLogs.Add(entry);
dbContext.SaveChanges();
EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
}
/// <inheritdoc/>
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<ActivityLogEntry>(ConvertToOldModel(entry)));
}
/// <inheritdoc/>
public QueryResult<ActivityLogEntry> GetPagedResult(
Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> 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<ActivityLogEntry>
{
Items = list,
TotalRecordCount = func(dbContext.ActivityLogs).Count()
};
}
/// <inheritdoc/>
public QueryResult<ActivityLogEntry> 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
};
}
}
}

View file

@ -25,6 +25,17 @@
<Compile Remove="Migrations\20200430214405_InitialSchema.Designer.cs" /> <Compile Remove="Migrations\20200430214405_InitialSchema.Designer.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" /> <ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />

View file

@ -15,6 +15,7 @@ namespace Jellyfin.Server.Implementations
/// <inheritdoc/> /// <inheritdoc/>
public partial class JellyfinDb : DbContext public partial class JellyfinDb : DbContext
{ {
public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
/*public virtual DbSet<Artwork> Artwork { get; set; } /*public virtual DbSet<Artwork> Artwork { get; set; }
public virtual DbSet<Book> Books { get; set; } public virtual DbSet<Book> Books { get; set; }
public virtual DbSet<BookMetadata> BookMetadata { get; set; } public virtual DbSet<BookMetadata> BookMetadata { get; set; }
@ -49,6 +50,7 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<Preference> Preferences { get; set; } public virtual DbSet<Preference> Preferences { get; set; }
public virtual DbSet<ProviderMapping> ProviderMappings { get; set; } public virtual DbSet<ProviderMapping> ProviderMappings { get; set; }
public virtual DbSet<Rating> Ratings { get; set; } public virtual DbSet<Rating> Ratings { get; set; }
/// <summary> /// <summary>
/// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
/// store review ratings, not age ratings /// store review ratings, not age ratings
@ -93,8 +95,10 @@ namespace Jellyfin.Server.Implementations
modelBuilder.HasDefaultSchema("jellyfin"); modelBuilder.HasDefaultSchema("jellyfin");
/*modelBuilder.Entity<Artwork>().HasIndex(t => t.Kind); /*modelBuilder.Entity<Artwork>().HasIndex(t => t.Kind);
modelBuilder.Entity<Genre>().HasIndex(t => t.Name) modelBuilder.Entity<Genre>().HasIndex(t => t.Name)
.IsUnique(); .IsUnique();
modelBuilder.Entity<LibraryItem>().HasIndex(t => t.UrlId) modelBuilder.Entity<LibraryItem>().HasIndex(t => t.UrlId)
.IsUnique();*/ .IsUnique();*/
@ -103,9 +107,10 @@ namespace Jellyfin.Server.Implementations
public override int SaveChanges() 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<ISavingChanges>())
{ {
var saveEntity = entity.Entity as ISavingChanges;
saveEntity.OnSavingChanges(); saveEntity.OnSavingChanges();
} }

View file

@ -0,0 +1,33 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Server.Implementations
{
/// <summary>
/// Factory class for generating new <see cref="JellyfinDb"/> instances.
/// </summary>
public class JellyfinDbProvider
{
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
/// </summary>
/// <param name="serviceProvider">The application's service provider.</param>
public JellyfinDbProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
serviceProvider.GetService<JellyfinDb>().Database.Migrate();
}
/// <summary>
/// Creates a new <see cref="JellyfinDb"/> context.
/// </summary>
/// <returns>The newly created context.</returns>
public JellyfinDb CreateContext()
{
return _serviceProvider.GetRequiredService<JellyfinDb>();
}
}
}

View file

@ -0,0 +1,72 @@
#pragma warning disable CS1591
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<string>("Overview")
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ActivityLogs");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -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<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(maxLength: 512, nullable: false),
Overview = table.Column<string>(maxLength: 512, nullable: true),
ShortOverview = table.Column<string>(maxLength: 512, nullable: true),
Type = table.Column<string>(maxLength: 256, nullable: false),
UserId = table.Column<Guid>(nullable: false),
ItemId = table.Column<string>(maxLength: 256, nullable: true),
DateCreated = table.Column<DateTime>(nullable: false),
LogSeverity = table.Column<int>(nullable: false),
RowVersion = table.Column<uint>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ActivityLogs", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ActivityLogs",
schema: "jellyfin");
}
}
}

View file

@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Jellyfin.Server.Implementations.Migrations
{
/// <summary>
/// The design time factory for <see cref="JellyfinDb"/>.
/// This is only used for the creation of migrations and not during runtime.
/// </summary>
internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory<JellyfinDb>
{
public JellyfinDb CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<JellyfinDb>();
optionsBuilder.UseSqlite("Data Source=jellyfin.db");
return new JellyfinDb(optionsBuilder.Options);
}
}
}

View file

@ -0,0 +1,66 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<string>("Overview")
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ActivityLogs");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -1,12 +1,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Reflection; using System.Reflection;
using Emby.Drawing; using Emby.Drawing;
using Emby.Server.Implementations; using Emby.Server.Implementations;
using Jellyfin.Drawing.Skia; using Jellyfin.Drawing.Skia;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -56,6 +61,15 @@ namespace Jellyfin.Server
Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}."); Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
} }
// TODO: Set up scoping and use AddDbContextPool
serviceCollection.AddDbContext<JellyfinDb>(
options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
ServiceLifetime.Transient);
serviceCollection.AddSingleton<JellyfinDbProvider>();
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
base.RegisterServices(serviceCollection); base.RegisterServices(serviceCollection);
} }

View file

@ -41,8 +41,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.7.82" /> <PackageReference Include="CommandLineParser" Version="2.7.82" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.4" />
<PackageReference Include="prometheus-net" Version="3.5.0" /> <PackageReference Include="prometheus-net" Version="3.5.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="3.5.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="3.5.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
@ -60,6 +60,7 @@
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" /> <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
<ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" /> <ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
<ProjectReference Include="..\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" /> <ProjectReference Include="..\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" />
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -17,7 +17,9 @@ namespace Jellyfin.Server.Migrations
private static readonly Type[] _migrationTypes = private static readonly Type[] _migrationTypes =
{ {
typeof(Routines.DisableTranscodingThrottling), typeof(Routines.DisableTranscodingThrottling),
typeof(Routines.CreateUserLoggingConfigFile) typeof(Routines.CreateUserLoggingConfigFile),
typeof(Routines.MigrateActivityLogDb),
typeof(Routines.RemoveDuplicateExtras)
}; };
/// <summary> /// <summary>

View file

@ -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
{
/// <summary>
/// The migration routine for migrating the activity log database to EF Core.
/// </summary>
public class MigrateActivityLogDb : IMigrationRoutine
{
private const string DbFilename = "activitylog.db";
private readonly ILogger<MigrateActivityLogDb> _logger;
private readonly JellyfinDbProvider _provider;
private readonly IServerApplicationPaths _paths;
/// <summary>
/// Initializes a new instance of the <see cref="MigrateActivityLogDb"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="paths">The server application paths.</param>
/// <param name="provider">The database provider.</param>
public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
{
_logger = logger;
_provider = provider;
_paths = paths;
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978");
/// <inheritdoc/>
public string Name => "MigrateActivityLogDatabase";
/// <inheritdoc/>
public void Perform()
{
var logLevelDictionary = new Dictionary<string, LogLevel>(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'");
}
}
}
}

View file

@ -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
{
/// <summary>
/// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself.
/// </summary>
internal class RemoveDuplicateExtras : IMigrationRoutine
{
private const string DbFilename = "library.db";
private readonly ILogger _logger;
private readonly IServerApplicationPaths _paths;
public RemoveDuplicateExtras(ILogger<RemoveDuplicateExtras> logger, IServerApplicationPaths paths)
{
_logger = logger;
_paths = paths;
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("{ACBE17B7-8435-4A83-8B64-6FCF162CB9BD}");
/// <inheritdoc/>
public string Name => "RemoveDuplicateExtras";
/// <inheritdoc/>
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')");
}
}
}
}

View file

@ -1,5 +1,4 @@
using System.IO; using System.IO;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
@ -116,11 +115,6 @@ namespace MediaBrowser.Api.Devices
return _deviceManager.GetDeviceOptions(request.Id); return _deviceManager.GetDeviceOptions(request.Id);
} }
public object Get(GetCameraUploads request)
{
return ToOptimizedResult(_deviceManager.GetCameraUploadHistory(request.DeviceId));
}
public void Delete(DeleteDevice request) public void Delete(DeleteDevice request)
{ {
var sessions = _authRepo.Get(new AuthenticationInfoQuery var sessions = _authRepo.Get(new AuthenticationInfoQuery
@ -134,35 +128,5 @@ namespace MediaBrowser.Api.Devices
_sessionManager.Logout(session); _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
});
}
} }
} }

View file

@ -763,13 +763,12 @@ namespace MediaBrowser.Api.Library
{ {
try 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), ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
UserId = auth.UserId
}); });
} }
catch catch

View file

@ -1,5 +1,7 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity; using MediaBrowser.Model.Activity;
@ -53,7 +55,10 @@ namespace MediaBrowser.Api.System
(DateTime?)null : (DateTime?)null :
DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
var result = _activityManager.GetActivityLogEntries(minDate, request.HasUserId, request.StartIndex, request.Limit); var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
entries => entries.Where(entry => entry.DateCreated >= minDate));
var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit);
return ToOptimizedResult(result); return ToOptimizedResult(result);
} }

View file

@ -17,8 +17,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup> </ItemGroup>

View file

@ -1,6 +1,4 @@
using System; using System;
using System.IO;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Devices; using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
@ -11,11 +9,6 @@ namespace MediaBrowser.Controller.Devices
{ {
public interface IDeviceManager public interface IDeviceManager
{ {
/// <summary>
/// Occurs when [camera image uploaded].
/// </summary>
event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
/// <summary> /// <summary>
/// Saves the capabilities. /// Saves the capabilities.
/// </summary> /// </summary>
@ -45,22 +38,6 @@ namespace MediaBrowser.Controller.Devices
/// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns> /// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
QueryResult<DeviceInfo> GetDevices(DeviceQuery query); QueryResult<DeviceInfo> GetDevices(DeviceQuery query);
/// <summary>
/// Gets the upload history.
/// </summary>
/// <param name="deviceId">The device identifier.</param>
/// <returns>ContentUploadHistory.</returns>
ContentUploadHistory GetCameraUploadHistory(string deviceId);
/// <summary>
/// Accepts the upload.
/// </summary>
/// <param name="deviceId">The device identifier.</param>
/// <param name="stream">The stream.</param>
/// <param name="file">The file.</param>
/// <returns>Task.</returns>
Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file);
/// <summary> /// <summary>
/// Determines whether this instance [can access device] the specified user identifier. /// Determines whether this instance [can access device] the specified user identifier.
/// </summary> /// </summary>

View file

@ -13,8 +13,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -278,5 +278,19 @@ namespace MediaBrowser.MediaEncoding.Probing
/// <value>The disposition.</value> /// <value>The disposition.</value>
[JsonPropertyName("disposition")] [JsonPropertyName("disposition")]
public IReadOnlyDictionary<string, int> Disposition { get; set; } public IReadOnlyDictionary<string, int> Disposition { get; set; }
/// <summary>
/// Gets or sets the color transfer.
/// </summary>
/// <value>The color transfer.</value>
[JsonPropertyName("color_transfer")]
public string ColorTransfer { get; set; }
/// <summary>
/// Gets or sets the color primaries.
/// </summary>
/// <value>The color primaries.</value>
[JsonPropertyName("color_primaries")]
public string ColorPrimaries { get; set; }
} }
} }

View file

@ -695,6 +695,16 @@ namespace MediaBrowser.MediaEncoding.Probing
{ {
stream.RefFrames = streamInfo.Refs; stream.RefFrames = streamInfo.Refs;
} }
if (!string.IsNullOrEmpty(streamInfo.ColorTransfer))
{
stream.ColorTransfer = streamInfo.ColorTransfer;
}
if (!string.IsNullOrEmpty(streamInfo.ColorPrimaries))
{
stream.ColorPrimaries = streamInfo.ColorPrimaries;
}
} }
else else
{ {

View file

@ -59,6 +59,7 @@ namespace MediaBrowser.Model.Activity
/// Gets or sets the user primary image tag. /// Gets or sets the user primary image tag.
/// </summary> /// </summary>
/// <value>The user primary image tag.</value> /// <value>The user primary image tag.</value>
[Obsolete("UserPrimaryImageTag is not used.")]
public string UserPrimaryImageTag { get; set; } public string UserPrimaryImageTag { get; set; }
/// <summary> /// <summary>

View file

@ -1,6 +1,9 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
@ -10,10 +13,15 @@ namespace MediaBrowser.Model.Activity
{ {
event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
void Create(ActivityLogEntry entry); void Create(ActivityLog entry);
QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit); Task CreateAsync(ActivityLog entry);
QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? x, int? y); QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit);
QueryResult<ActivityLogEntry> GetPagedResult(
Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>> func,
int? startIndex,
int? limit);
} }
} }

View file

@ -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<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? z, int? startIndex, int? limit);
}
}

View file

@ -79,8 +79,6 @@ namespace MediaBrowser.Model.Configuration
public bool EnableRemoteAccess { get; set; } public bool EnableRemoteAccess { get; set; }
public bool CameraUploadUpgraded { get; set; }
public bool CollectionsUpgraded { get; set; } public bool CollectionsUpgraded { get; set; }
/// <summary> /// <summary>

View file

@ -0,0 +1,9 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Devices
{
public class DeviceOptions
{
public string CustomName { get; set; }
}
}

View file

@ -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<string>();
}
}
public class DeviceOptions
{
public string CustomName { get; set; }
}
}

View file

@ -34,8 +34,22 @@ namespace MediaBrowser.Model.Entities
/// <value>The language.</value> /// <value>The language.</value>
public string Language { get; set; } public string Language { get; set; }
/// <summary>
/// Gets or sets the color transfer.
/// </summary>
/// <value>The color transfer.</value>
public string ColorTransfer { get; set; } public string ColorTransfer { get; set; }
/// <summary>
/// Gets or sets the color primaries.
/// </summary>
/// <value>The color primaries.</value>
public string ColorPrimaries { get; set; } public string ColorPrimaries { get; set; }
/// <summary>
/// Gets or sets the color space.
/// </summary>
/// <value>The color space.</value>
public string ColorSpace { get; set; } public string ColorSpace { get; set; }
/// <summary> /// <summary>
@ -44,11 +58,28 @@ namespace MediaBrowser.Model.Entities
/// <value>The comment.</value> /// <value>The comment.</value>
public string Comment { get; set; } public string Comment { get; set; }
/// <summary>
/// Gets or sets the time base.
/// </summary>
/// <value>The time base.</value>
public string TimeBase { get; set; } public string TimeBase { get; set; }
/// <summary>
/// Gets or sets the codec time base.
/// </summary>
/// <value>The codec time base.</value>
public string CodecTimeBase { get; set; } public string CodecTimeBase { get; set; }
/// <summary>
/// Gets or sets the title.
/// </summary>
/// <value>The title.</value>
public string Title { get; set; } public string Title { get; set; }
/// <summary>
/// Gets or sets the video range.
/// </summary>
/// <value>The video range.</value>
public string VideoRange public string VideoRange
{ {
get get
@ -60,7 +91,8 @@ namespace MediaBrowser.Model.Entities
var colorTransfer = ColorTransfer; 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"; return "HDR";
} }
@ -70,7 +102,9 @@ namespace MediaBrowser.Model.Entities
} }
public string localizedUndefined { get; set; } public string localizedUndefined { get; set; }
public string localizedDefault { get; set; } public string localizedDefault { get; set; }
public string localizedForced { get; set; } public string localizedForced { get; set; }
public string DisplayTitle public string DisplayTitle
@ -197,34 +231,34 @@ namespace MediaBrowser.Model.Entities
{ {
if (i.IsInterlaced) if (i.IsInterlaced)
{ {
return "1440I"; return "1440i";
} }
return "1440P"; return "1440p";
} }
if (width >= 1900 || height >= 1000) if (width >= 1900 || height >= 1000)
{ {
if (i.IsInterlaced) if (i.IsInterlaced)
{ {
return "1080I"; return "1080i";
} }
return "1080P"; return "1080p";
} }
if (width >= 1260 || height >= 700) if (width >= 1260 || height >= 700)
{ {
if (i.IsInterlaced) if (i.IsInterlaced)
{ {
return "720I"; return "720i";
} }
return "720P"; return "720p";
} }
if (width >= 700 || height >= 440) if (width >= 700 || height >= 440)
{ {
if (i.IsInterlaced) if (i.IsInterlaced)
{ {
return "480I"; return "480i";
} }
return "480P"; return "480p";
} }
return "SD"; return "SD";

View file

@ -21,9 +21,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.4" />
<PackageReference Include="System.Globalization" Version="4.3.0" /> <PackageReference Include="System.Globalization" Version="4.3.0" />
<PackageReference Include="System.Text.Json" Version="4.7.1" /> <PackageReference Include="System.Text.Json" Version="4.7.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -37,6 +37,9 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>

View file

@ -67,6 +67,7 @@ namespace MediaBrowser.Model.Net
{ ".m3u8", "application/x-mpegURL" }, { ".m3u8", "application/x-mpegURL" },
{ ".map", "application/x-javascript" }, { ".map", "application/x-javascript" },
{ ".mobi", "application/x-mobipocket-ebook" }, { ".mobi", "application/x-mobipocket-ebook" },
{ ".opf", "application/oebps-package+xml" },
{ ".pdf", "application/pdf" }, { ".pdf", "application/pdf" },
{ ".rar", "application/vnd.rar" }, { ".rar", "application/vnd.rar" },
{ ".srt", "application/x-subrip" }, { ".srt", "application/x-subrip" },
@ -99,6 +100,7 @@ namespace MediaBrowser.Model.Net
{ ".ssa", "text/x-ssa" }, { ".ssa", "text/x-ssa" },
{ ".css", "text/css" }, { ".css", "text/css" },
{ ".csv", "text/csv" }, { ".csv", "text/csv" },
{ ".edl", "text/plain" },
{ ".rtf", "text/rtf" }, { ".rtf", "text/rtf" },
{ ".txt", "text/plain" }, { ".txt", "text/plain" },
{ ".vtt", "text/vtt" }, { ".vtt", "text/vtt" },

View file

@ -16,8 +16,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.4" />
<PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" /> <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
<PackageReference Include="PlaylistsNET" Version="1.0.4" /> <PackageReference Include="PlaylistsNET" Version="1.0.4" />
<PackageReference Include="TvDbSharper" Version="3.0.1" /> <PackageReference Include="TvDbSharper" Version="3.0.1" />

View file

@ -31,8 +31,8 @@
$('.configPage').on('pageshow', function () { $('.configPage').on('pageshow', function () {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
$('#enable').checked(config.Enable); $('#enable').checked = config.Enable;
$('#replaceAlbumName').checked(config.ReplaceAlbumName); $('#replaceAlbumName').checked = config.ReplaceAlbumName;
Dashboard.hideLoadingMsg(); Dashboard.hideLoadingMsg();
}); });
@ -43,8 +43,8 @@
var form = this; var form = this;
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
config.Enable = $('#enable', form).checked(); config.Enable = $('#enable', form).checked;
config.ReplaceAlbumName = $('#replaceAlbumName', form).checked(); config.ReplaceAlbumName = $('#replaceAlbumName', form).checked;
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
}); });

View file

@ -41,8 +41,8 @@
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
$('#server').val(config.Server).change(); $('#server').val(config.Server).change();
$('#rateLimit').val(config.RateLimit).change(); $('#rateLimit').val(config.RateLimit).change();
$('#enable').checked(config.Enable); $('#enable').checked = config.Enable;
$('#replaceArtistName').checked(config.ReplaceArtistName); $('#replaceArtistName').checked = config.ReplaceArtistName;
Dashboard.hideLoadingMsg(); Dashboard.hideLoadingMsg();
}); });
@ -55,8 +55,8 @@
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
config.Server = $('#server', form).val(); config.Server = $('#server', form).val();
config.RateLimit = $('#rateLimit', form).val(); config.RateLimit = $('#rateLimit', form).val();
config.Enable = $('#enable', form).checked(); config.Enable = $('#enable', form).checked;
config.ReplaceArtistName = $('#replaceArtistName', form).checked(); config.ReplaceArtistName = $('#replaceArtistName', form).checked;
ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
}); });

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Text.RegularExpressions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -18,8 +19,22 @@ namespace MediaBrowser.Providers.Tmdb.Movies
{ {
public class TmdbSearch public class TmdbSearch
{ {
private static readonly CultureInfo EnUs = new CultureInfo("en-US"); private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
private const string Search3 = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}";
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 ILogger _logger;
private readonly IJsonSerializer _json; private readonly IJsonSerializer _json;
@ -61,19 +76,18 @@ namespace MediaBrowser.Providers.Tmdb.Movies
var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
if (!string.IsNullOrWhiteSpace(name)) // TODO: Investigate: Does this mean we are reparsing already parsed ItemLookupInfo?
{ var parsedName = _libraryManager.ParseName(name);
var parsedName = _libraryManager.ParseName(name); var yearInName = parsedName.Year;
var yearInName = parsedName.Year; name = parsedName.Name;
name = parsedName.Name; year ??= yearInName;
year = 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(); var language = idInfo.MetadataLanguage.ToLowerInvariant();
//nope - search for it // Replace sequences of non-word characters with space
//var searchType = item is BoxSet ? "collection" : "movie"; // 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); 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) if (results.Count == 0)
{ {
// try with dot and _ turned to space var name2 = parsedName.Name;
var originalName = name;
name = name.Replace(",", " "); // Remove things enclosed in []{}() etc
name = name.Replace(".", " "); name2 = _cleanEnclosed.Replace(name2, string.Empty);
name = name.Replace("_", " ");
name = name.Replace("-", " ");
name = name.Replace("!", " ");
name = name.Replace("?", " ");
var parenthIndex = name.IndexOf('('); // Replace sequences of non-word characters with space
if (parenthIndex != -1) name2 = _cleanNonWord.Replace(name2, " ");
{
name = name.Substring(0, parenthIndex);
}
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 // 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)) if (results.Count == 0 && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
{ {
//one more time, in english //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"); 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 using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
{ {
@ -179,14 +192,14 @@ namespace MediaBrowser.Providers.Tmdb.Movies
if (!string.IsNullOrWhiteSpace(i.Release_Date)) if (!string.IsNullOrWhiteSpace(i.Release_Date))
{ {
// These dates are always in this exact format // 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.PremiereDate = r.ToUniversalTime();
remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
} }
} }
remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(EnUs)); remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(_usCulture));
return remoteResult; return remoteResult;
@ -203,7 +216,7 @@ namespace MediaBrowser.Providers.Tmdb.Movies
throw new ArgumentException("name"); 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 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)) if (!string.IsNullOrWhiteSpace(i.First_Air_Date))
{ {
// These dates are always in this exact format // 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.PremiereDate = r.ToUniversalTime();
remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
} }
} }
remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(EnUs)); remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(_usCulture));
return remoteResult; return remoteResult;

View file

@ -66,9 +66,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}"
EndProject 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 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}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api.Tests", "tests\MediaBrowser.Api.Tests\MediaBrowser.Api.Tests.csproj", "{7C93C84F-105C-48E5-A878-406FA0A5B296}"
EndProject EndProject
Global Global
@ -185,10 +186,10 @@ Global
{F03299F2-469F-40EF-A655-3766F97A5702}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.Build.0 = 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 {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.Build.0 = Debug|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.ActiveCfg = Release|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.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}.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.ActiveCfg = Debug|Any CPU
{7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.Build.0 = 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 {7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.ActiveCfg = Release|Any CPU

View file

@ -16,7 +16,7 @@
<PackageReference Include="AutoFixture" Version="4.11.0" /> <PackageReference Include="AutoFixture" Version="4.11.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.11.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.11.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup> <PropertyGroup>
@ -21,5 +21,15 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Emby.Naming\Emby.Naming.csproj" /> <ProjectReference Include="..\..\Emby.Naming\Emby.Naming.csproj" />
</ItemGroup> </ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project> </Project>

View file

@ -23,9 +23,9 @@ namespace Jellyfin.Naming.Tests.Subtitles
var result = parser.ParseFile(input); var result = parser.ParseFile(input);
Assert.Equal(language, result.Language, true); Assert.Equal(language, result?.Language, true);
Assert.Equal(isDefault, result.IsDefault); Assert.Equal(isDefault, result?.IsDefault);
Assert.Equal(isForced, result.IsForced); Assert.Equal(isForced, result?.IsForced);
} }
[Theory] [Theory]

View file

@ -21,7 +21,7 @@ namespace Jellyfin.Naming.Tests.TV
var result = new EpisodeResolver(options) var result = new EpisodeResolver(options)
.Resolve(path, false, null, null, true); .Resolve(path, false, null, null, true);
Assert.Equal(episodeNumber, result.EpisodeNumber); Assert.Equal(episodeNumber, result?.EpisodeNumber);
} }
} }
} }

View file

@ -6,8 +6,6 @@ namespace Jellyfin.Naming.Tests.TV
{ {
public class DailyEpisodeTests public class DailyEpisodeTests
{ {
[Theory] [Theory]
[InlineData(@"/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)] [InlineData(@"/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)]
[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) var result = new EpisodeResolver(options)
.Resolve(path, false); .Resolve(path, false);
Assert.Null(result.SeasonNumber); Assert.Null(result?.SeasonNumber);
Assert.Null(result.EpisodeNumber); Assert.Null(result?.EpisodeNumber);
Assert.Equal(year, result.Year); Assert.Equal(year, result?.Year);
Assert.Equal(month, result.Month); Assert.Equal(month, result?.Month);
Assert.Equal(day, result.Day); Assert.Equal(day, result?.Day);
Assert.Equal(seriesName, result.SeriesName, true); Assert.Equal(seriesName, result?.SeriesName, true);
} }
} }
} }

View file

@ -6,7 +6,6 @@ namespace Jellyfin.Naming.Tests.TV
{ {
public class EpisodeNumberWithoutSeasonTests public class EpisodeNumberWithoutSeasonTests
{ {
[Theory] [Theory]
[InlineData(8, @"The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")] [InlineData(8, @"The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")]
[InlineData(2, @"The Simpsons/The Simpsons - 02 - Ep Name.avi")] [InlineData(2, @"The Simpsons/The Simpsons - 02 - Ep Name.avi")]
@ -30,7 +29,7 @@ namespace Jellyfin.Naming.Tests.TV
var result = new EpisodeResolver(options) var result = new EpisodeResolver(options)
.Resolve(path, false); .Resolve(path, false);
Assert.Equal(episodeNumber, result.EpisodeNumber); Assert.Equal(episodeNumber, result?.EpisodeNumber);
} }
} }
} }

View file

@ -1,4 +1,4 @@
using Emby.Naming.Common; using Emby.Naming.Common;
using Emby.Naming.TV; using Emby.Naming.TV;
using Xunit; 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("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)] // 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) public void ParseEpisodesCorrectly(string path, string name, int season, int episode)
{ {
NamingOptions o = new NamingOptions(); NamingOptions o = new NamingOptions();
EpisodePathParser p = new EpisodePathParser(o); EpisodePathParser p = new EpisodePathParser(o);

View file

@ -19,9 +19,9 @@ namespace Jellyfin.Naming.Tests.TV
var result = new EpisodeResolver(options) var result = new EpisodeResolver(options)
.Resolve(path, false); .Resolve(path, false);
Assert.Equal(seasonNumber, result.SeasonNumber); Assert.Equal(seasonNumber, result?.SeasonNumber);
Assert.Equal(episodeNumber, result.EpisodeNumber); Assert.Equal(episodeNumber, result?.EpisodeNumber);
Assert.Equal(seriesName, result.SeriesName, true); Assert.Equal(seriesName, result?.SeriesName, ignoreCase: true);
} }
} }
} }

View file

@ -59,7 +59,7 @@ namespace Jellyfin.Naming.Tests.TV
var result = new EpisodeResolver(_namingOptions) var result = new EpisodeResolver(_namingOptions)
.Resolve(path, false); .Resolve(path, false);
Assert.Equal(expected, result.SeasonNumber); Assert.Equal(expected, result?.SeasonNumber);
} }
} }
} }

View file

@ -31,9 +31,9 @@ namespace Jellyfin.Naming.Tests.TV
var result = new EpisodeResolver(options) var result = new EpisodeResolver(options)
.Resolve(path, false); .Resolve(path, false);
Assert.Equal(seasonNumber, result.SeasonNumber); Assert.Equal(seasonNumber, result?.SeasonNumber);
Assert.Equal(episodeNumber, result.EpisodeNumber); Assert.Equal(episodeNumber, result?.EpisodeNumber);
Assert.Equal(seriesName, result.SeriesName, true); Assert.Equal(seriesName, result?.SeriesName, true);
} }
} }
} }

View file

@ -25,8 +25,8 @@ namespace Jellyfin.Naming.Tests.Video
var result = var result =
new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv"); new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv");
Assert.Equal("hsbs", result.Format3D); Assert.Equal("hsbs", result?.Format3D);
Assert.Equal("Oblivion", result.Name); Assert.Equal("Oblivion", result?.Name);
} }
[Fact] [Fact]

View file

@ -12,7 +12,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiEdition1() private void TestMultiEdition1()
{ {
var files = new[] var files = new[]
{ {
@ -28,7 +28,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -37,7 +36,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiEdition2() private void TestMultiEdition2()
{ {
var files = new[] var files = new[]
{ {
@ -53,7 +52,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -76,7 +74,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -85,7 +82,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestLetterFolders() private void TestLetterFolders()
{ {
var files = new[] var files = new[]
{ {
@ -104,7 +101,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Equal(7, result.Count); Assert.Equal(7, result.Count);
@ -114,7 +110,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiVersionLimit() private void TestMultiVersionLimit()
{ {
var files = new[] var files = new[]
{ {
@ -134,7 +130,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -144,7 +139,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiVersionLimit2() private void TestMultiVersionLimit2()
{ {
var files = new[] var files = new[]
{ {
@ -165,7 +160,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Equal(9, result.Count); Assert.Equal(9, result.Count);
@ -175,7 +169,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiVersion3() private void TestMultiVersion3()
{ {
var files = new[] var files = new[]
{ {
@ -192,7 +186,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Equal(5, result.Count); Assert.Equal(5, result.Count);
@ -202,7 +195,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiVersion4() private void TestMultiVersion4()
{ {
// Test for false positive // Test for false positive
@ -221,7 +214,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Equal(5, result.Count); Assert.Equal(5, result.Count);
@ -231,7 +223,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiVersion5() private void TestMultiVersion5()
{ {
var files = new[] var files = new[]
{ {
@ -251,7 +243,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -264,7 +255,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiVersion6() private void TestMultiVersion6()
{ {
var files = new[] var files = new[]
{ {
@ -284,7 +275,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -297,7 +287,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiVersion7() private void TestMultiVersion7()
{ {
var files = new[] var files = new[]
{ {
@ -311,7 +301,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
@ -319,7 +308,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiVersion8() private void TestMultiVersion8()
{ {
// This is not actually supported yet // This is not actually supported yet
@ -340,7 +329,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -353,7 +341,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiVersion9() private void TestMultiVersion9()
{ {
// Test for false positive // Test for false positive
@ -372,7 +360,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Equal(5, result.Count); Assert.Equal(5, result.Count);
@ -382,7 +369,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiVersion10() private void TestMultiVersion10()
{ {
var files = new[] var files = new[]
{ {
@ -396,7 +383,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -406,7 +392,7 @@ namespace Jellyfin.Naming.Tests.Video
// FIXME // FIXME
// [Fact] // [Fact]
public void TestMultiVersion11() private void TestMultiVersion11()
{ {
// Currently not supported but we should probably handle this. // Currently not supported but we should probably handle this.
@ -422,7 +408,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);

View file

@ -368,11 +368,11 @@ namespace Jellyfin.Naming.Tests.Video
{ {
var files = new[] var files = new[]
{ {
new FileSystemMetadata{FullName = "Bad Boys (2006) part1.mkv", IsDirectory = false}, new FileSystemMetadata { FullName = "Bad Boys (2006) part1.mkv", IsDirectory = false },
new FileSystemMetadata{FullName = "Bad Boys (2006) part2.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) part2", IsDirectory = true },
new FileSystemMetadata{FullName = "300 (2006) part3", IsDirectory = true}, new FileSystemMetadata { FullName = "300 (2006) part3", IsDirectory = true },
new FileSystemMetadata{FullName = "300 (2006) part1", IsDirectory = true} new FileSystemMetadata { FullName = "300 (2006) part1", IsDirectory = true }
}; };
var resolver = GetResolver(); var resolver = GetResolver();

View file

@ -31,10 +31,10 @@ namespace Jellyfin.Naming.Tests.Video
var result = var result =
new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc"); 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); var isStubResult = StubResolver.TryResolveFile(path, _namingOptions, out var stubTypeResult);

View file

@ -9,9 +9,10 @@ namespace Jellyfin.Naming.Tests.Video
public class VideoListResolverTests public class VideoListResolverTests
{ {
private readonly NamingOptions _namingOptions = new NamingOptions(); private readonly NamingOptions _namingOptions = new NamingOptions();
// FIXME // FIXME
// [Fact] // [Fact]
public void TestStackAndExtras() private void TestStackAndExtras()
{ {
// No stacking here because there is no part/disc/etc // No stacking here because there is no part/disc/etc
var files = new[] var files = new[]
@ -45,7 +46,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Equal(5, result.Count); Assert.Equal(5, result.Count);
@ -74,7 +74,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -95,7 +94,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -116,7 +114,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -138,7 +135,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -159,7 +155,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -184,7 +179,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Equal(5, result.Count); Assert.Equal(5, result.Count);
@ -205,7 +199,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = true, IsDirectory = true,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -227,7 +220,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = true, IsDirectory = true,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
@ -249,7 +241,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -271,7 +262,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -294,7 +284,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -317,7 +306,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Equal(2, result.Count); Assert.Equal(2, result.Count);
@ -337,7 +325,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -357,7 +344,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -378,7 +364,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -399,7 +384,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);
@ -422,7 +406,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Equal(4, result.Count); Assert.Equal(4, result.Count);
@ -443,7 +426,6 @@ namespace Jellyfin.Naming.Tests.Video
{ {
IsDirectory = false, IsDirectory = false,
FullName = i FullName = i
}).ToList()).ToList(); }).ToList()).ToList();
Assert.Single(result); Assert.Single(result);

View file

@ -176,7 +176,6 @@ namespace Jellyfin.Naming.Tests.Video
}; };
} }
[Theory] [Theory]
[MemberData(nameof(GetResolveFileTestData))] [MemberData(nameof(GetResolveFileTestData))]
public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult) public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult)
@ -184,17 +183,17 @@ namespace Jellyfin.Naming.Tests.Video
var result = new VideoResolver(_namingOptions).ResolveFile(expectedResult.Path); var result = new VideoResolver(_namingOptions).ResolveFile(expectedResult.Path);
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal(result.Path, expectedResult.Path); Assert.Equal(result?.Path, expectedResult.Path);
Assert.Equal(result.Container, expectedResult.Container); Assert.Equal(result?.Container, expectedResult.Container);
Assert.Equal(result.Name, expectedResult.Name); Assert.Equal(result?.Name, expectedResult.Name);
Assert.Equal(result.Year, expectedResult.Year); Assert.Equal(result?.Year, expectedResult.Year);
Assert.Equal(result.ExtraType, expectedResult.ExtraType); Assert.Equal(result?.ExtraType, expectedResult.ExtraType);
Assert.Equal(result.Format3D, expectedResult.Format3D); Assert.Equal(result?.Format3D, expectedResult.Format3D);
Assert.Equal(result.Is3D, expectedResult.Is3D); Assert.Equal(result?.Is3D, expectedResult.Is3D);
Assert.Equal(result.IsStub, expectedResult.IsStub); Assert.Equal(result?.IsStub, expectedResult.IsStub);
Assert.Equal(result.StubType, expectedResult.StubType); Assert.Equal(result?.StubType, expectedResult.StubType);
Assert.Equal(result.IsDirectory, expectedResult.IsDirectory); Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory);
Assert.Equal(result.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension); Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension);
} }
} }
} }

View file

@ -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));
}
}
}

View file

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />