mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 18:08:53 -07:00
Merge remote-tracking branch 'upstream/master' into syncplay
This commit is contained in:
commit
e4838b0faa
@ -2,7 +2,7 @@ ARG DOTNET_VERSION=3.1
|
||||
|
||||
FROM node:alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm \
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& cd jellyfin-web-* \
|
||||
&& yarn install \
|
||||
|
@ -38,7 +38,7 @@ COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
|
||||
curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
|
||||
curl -s https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
|
||||
curl -ks https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
|
||||
echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \
|
||||
echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
|
||||
apt-get update && \
|
||||
|
@ -227,7 +227,7 @@ namespace Emby.Naming.Video
|
||||
}
|
||||
|
||||
return remainingFiles
|
||||
.Where(i => i.ExtraType == null)
|
||||
.Where(i => i.ExtraType != null)
|
||||
.Where(i => baseNames.Any(b =>
|
||||
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
|
@ -4,11 +4,10 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Session;
|
||||
@ -30,7 +29,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
/// </summary>
|
||||
public sealed class ActivityLogEntryPoint : IServerEntryPoint
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILogger<ActivityLogEntryPoint> _logger;
|
||||
private readonly IInstallationManager _installationManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly ITaskManager _taskManager;
|
||||
@ -38,14 +37,12 @@ namespace Emby.Server.Implementations.Activity
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly ISubtitleManager _subManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
/// <param name="deviceManager">The device manager.</param>
|
||||
/// <param name="taskManager">The task manager.</param>
|
||||
/// <param name="activityManager">The activity manager.</param>
|
||||
/// <param name="localization">The localization manager.</param>
|
||||
@ -55,7 +52,6 @@ namespace Emby.Server.Implementations.Activity
|
||||
public ActivityLogEntryPoint(
|
||||
ILogger<ActivityLogEntryPoint> logger,
|
||||
ISessionManager sessionManager,
|
||||
IDeviceManager deviceManager,
|
||||
ITaskManager taskManager,
|
||||
IActivityManager activityManager,
|
||||
ILocalizationManager localization,
|
||||
@ -65,7 +61,6 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
_deviceManager = deviceManager;
|
||||
_taskManager = taskManager;
|
||||
_activityManager = activityManager;
|
||||
_localization = localization;
|
||||
@ -99,52 +94,38 @@ namespace Emby.Server.Implementations.Activity
|
||||
_userManager.UserPolicyUpdated += OnUserPolicyUpdated;
|
||||
_userManager.UserLockedOut += OnUserLockedOut;
|
||||
|
||||
_deviceManager.CameraImageUploaded += OnCameraImageUploaded;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
|
||||
private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("CameraImageUploadedFrom"),
|
||||
e.Argument.Device.Name),
|
||||
Type = NotificationType.CameraImageUploaded.ToString()
|
||||
});
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserLockedOutWithName"),
|
||||
e.Argument.Name),
|
||||
NotificationType.UserLockedOut.ToString(),
|
||||
e.Argument.Id))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnUserLockedOut(object sender, GenericEventArgs<User> e)
|
||||
private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserLockedOutWithName"),
|
||||
e.Argument.Name),
|
||||
Type = NotificationType.UserLockedOut.ToString(),
|
||||
UserId = e.Argument.Id
|
||||
});
|
||||
}
|
||||
|
||||
private void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
|
||||
e.Provider,
|
||||
Notifications.NotificationEntryPoint.GetItemName(e.Item)),
|
||||
Type = "SubtitleDownloadFailure",
|
||||
"SubtitleDownloadFailure",
|
||||
Guid.Empty)
|
||||
{
|
||||
ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
ShortOverview = e.Exception.Message
|
||||
});
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
|
||||
private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
|
||||
{
|
||||
var item = e.MediaInfo;
|
||||
|
||||
@ -167,20 +148,19 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
var user = e.Users[0];
|
||||
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
|
||||
user.Name,
|
||||
GetItemName(item),
|
||||
e.DeviceName),
|
||||
Type = GetPlaybackStoppedNotificationType(item.MediaType),
|
||||
UserId = user.Id
|
||||
});
|
||||
GetPlaybackStoppedNotificationType(item.MediaType),
|
||||
user.Id))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
|
||||
private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
|
||||
{
|
||||
var item = e.MediaInfo;
|
||||
|
||||
@ -203,17 +183,16 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
var user = e.Users.First();
|
||||
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
|
||||
user.Name,
|
||||
GetItemName(item),
|
||||
e.DeviceName),
|
||||
Type = GetPlaybackNotificationType(item.MediaType),
|
||||
UserId = user.Id
|
||||
});
|
||||
GetPlaybackNotificationType(item.MediaType),
|
||||
user.Id))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string GetItemName(BaseItemDto item)
|
||||
@ -263,7 +242,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnSessionEnded(object sender, SessionEventArgs e)
|
||||
private async void OnSessionEnded(object sender, SessionEventArgs e)
|
||||
{
|
||||
var session = e.SessionInfo;
|
||||
|
||||
@ -272,110 +251,108 @@ namespace Emby.Server.Implementations.Activity
|
||||
return;
|
||||
}
|
||||
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserOfflineFromDevice"),
|
||||
session.UserName,
|
||||
session.DeviceName),
|
||||
Type = "SessionEnded",
|
||||
"SessionEnded",
|
||||
session.UserId)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("LabelIpAddressValue"),
|
||||
session.RemoteEndPoint),
|
||||
UserId = session.UserId
|
||||
});
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
|
||||
private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
|
||||
{
|
||||
var user = e.Argument.User;
|
||||
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
|
||||
user.Name),
|
||||
Type = "AuthenticationSucceeded",
|
||||
"AuthenticationSucceeded",
|
||||
user.Id)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("LabelIpAddressValue"),
|
||||
e.Argument.SessionInfo.RemoteEndPoint),
|
||||
UserId = user.Id
|
||||
});
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
|
||||
private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
|
||||
e.Argument.Username),
|
||||
Type = "AuthenticationFailed",
|
||||
"AuthenticationFailed",
|
||||
Guid.Empty)
|
||||
{
|
||||
LogSeverity = LogLevel.Error,
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("LabelIpAddressValue"),
|
||||
e.Argument.RemoteEndPoint),
|
||||
Severity = LogLevel.Error
|
||||
});
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
|
||||
private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserPolicyUpdatedWithName"),
|
||||
e.Argument.Name),
|
||||
Type = "UserPolicyUpdated",
|
||||
UserId = e.Argument.Id
|
||||
});
|
||||
"UserPolicyUpdated",
|
||||
e.Argument.Id))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnUserDeleted(object sender, GenericEventArgs<User> e)
|
||||
private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserDeletedWithName"),
|
||||
e.Argument.Name),
|
||||
Type = "UserDeleted"
|
||||
});
|
||||
"UserDeleted",
|
||||
Guid.Empty))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
|
||||
private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserPasswordChangedWithName"),
|
||||
e.Argument.Name),
|
||||
Type = "UserPasswordChanged",
|
||||
UserId = e.Argument.Id
|
||||
});
|
||||
"UserPasswordChanged",
|
||||
e.Argument.Id))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnUserCreated(object sender, GenericEventArgs<User> e)
|
||||
private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserCreatedWithName"),
|
||||
e.Argument.Name),
|
||||
Type = "UserCreated",
|
||||
UserId = e.Argument.Id
|
||||
});
|
||||
"UserCreated",
|
||||
e.Argument.Id))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnSessionStarted(object sender, SessionEventArgs e)
|
||||
private async void OnSessionStarted(object sender, SessionEventArgs e)
|
||||
{
|
||||
var session = e.SessionInfo;
|
||||
|
||||
@ -384,87 +361,90 @@ namespace Emby.Server.Implementations.Activity
|
||||
return;
|
||||
}
|
||||
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserOnlineFromDevice"),
|
||||
session.UserName,
|
||||
session.DeviceName),
|
||||
Type = "SessionStarted",
|
||||
"SessionStarted",
|
||||
session.UserId)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("LabelIpAddressValue"),
|
||||
session.RemoteEndPoint),
|
||||
UserId = session.UserId
|
||||
});
|
||||
session.RemoteEndPoint)
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e)
|
||||
private async void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("PluginUpdatedWithName"),
|
||||
e.Argument.Item1.Name),
|
||||
Type = NotificationType.PluginUpdateInstalled.ToString(),
|
||||
NotificationType.PluginUpdateInstalled.ToString(),
|
||||
Guid.Empty)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("VersionNumber"),
|
||||
e.Argument.Item2.version),
|
||||
Overview = e.Argument.Item2.changelog
|
||||
});
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
|
||||
private async void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("PluginUninstalledWithName"),
|
||||
e.Argument.Name),
|
||||
Type = NotificationType.PluginUninstalled.ToString()
|
||||
});
|
||||
NotificationType.PluginUninstalled.ToString(),
|
||||
Guid.Empty))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e)
|
||||
private async void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("PluginInstalledWithName"),
|
||||
e.Argument.name),
|
||||
Type = NotificationType.PluginInstalled.ToString(),
|
||||
NotificationType.PluginInstalled.ToString(),
|
||||
Guid.Empty)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("VersionNumber"),
|
||||
e.Argument.version)
|
||||
});
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
|
||||
private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
|
||||
{
|
||||
var installationInfo = e.InstallationInfo;
|
||||
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("NameInstallFailed"),
|
||||
installationInfo.Name),
|
||||
Type = NotificationType.InstallationFailed.ToString(),
|
||||
NotificationType.InstallationFailed.ToString(),
|
||||
Guid.Empty)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("VersionNumber"),
|
||||
installationInfo.Version),
|
||||
Overview = e.Exception.Message
|
||||
});
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
|
||||
private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
|
||||
{
|
||||
var result = e.Result;
|
||||
var task = e.Task;
|
||||
@ -495,22 +475,20 @@ namespace Emby.Server.Implementations.Activity
|
||||
vals.Add(e.Result.LongErrorMessage);
|
||||
}
|
||||
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
|
||||
NotificationType.TaskFailed.ToString(),
|
||||
Guid.Empty)
|
||||
{
|
||||
Name = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("ScheduledTaskFailedWithName"),
|
||||
task.Name),
|
||||
Type = NotificationType.TaskFailed.ToString(),
|
||||
LogSeverity = LogLevel.Error,
|
||||
Overview = string.Join(Environment.NewLine, vals),
|
||||
ShortOverview = runningTime,
|
||||
Severity = LogLevel.Error
|
||||
});
|
||||
ShortOverview = runningTime
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateLogEntry(ActivityLogEntry entry)
|
||||
=> _activityManager.Create(entry);
|
||||
private async Task CreateLogEntry(ActivityLog entry)
|
||||
=> await _activityManager.CreateAsync(entry).ConfigureAwait(false);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
@ -537,8 +515,6 @@ namespace Emby.Server.Implementations.Activity
|
||||
_userManager.UserDeleted -= OnUserDeleted;
|
||||
_userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
|
||||
_userManager.UserLockedOut -= OnUserLockedOut;
|
||||
|
||||
_deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -566,7 +542,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
int months = days / DaysInMonth;
|
||||
values.Add(CreateValueString(months, "month"));
|
||||
days %= DaysInMonth;
|
||||
days = days % DaysInMonth;
|
||||
}
|
||||
|
||||
// Number of days
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,6 @@ using Emby.Dlna.Ssdp;
|
||||
using Emby.Drawing;
|
||||
using Emby.Notifications;
|
||||
using Emby.Photos;
|
||||
using Emby.Server.Implementations.Activity;
|
||||
using Emby.Server.Implementations.Archiving;
|
||||
using Emby.Server.Implementations.Channels;
|
||||
using Emby.Server.Implementations.Collections;
|
||||
@ -83,7 +82,6 @@ using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Controller.SyncPlay;
|
||||
using MediaBrowser.LocalMetadata.Savers;
|
||||
using MediaBrowser.MediaEncoding.BdInfo;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
@ -632,9 +630,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IActivityRepository, ActivityRepository>();
|
||||
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
|
||||
serviceCollection.AddSingleton<ISessionContext, SessionContext>();
|
||||
|
||||
@ -665,7 +660,6 @@ namespace Emby.Server.Implementations
|
||||
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
|
||||
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
||||
((SqliteUserRepository)Resolve<IUserRepository>()).Initialize();
|
||||
((ActivityRepository)Resolve<IActivityRepository>()).Initialize();
|
||||
|
||||
SetStaticProperties();
|
||||
|
||||
|
@ -193,12 +193,6 @@ namespace Emby.Server.Implementations.Configuration
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!config.CameraUploadUpgraded)
|
||||
{
|
||||
config.CameraUploadUpgraded = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!config.CollectionsUpgraded)
|
||||
{
|
||||
config.CollectionsUpgraded = true;
|
||||
|
@ -5,27 +5,18 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Devices;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using MediaBrowser.Model.Users;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Devices
|
||||
{
|
||||
@ -33,38 +24,23 @@ namespace Emby.Server.Implementations.Devices
|
||||
{
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILibraryMonitor _libraryMonitor;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localizationManager;
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
|
||||
|
||||
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();
|
||||
|
||||
public DeviceManager(
|
||||
IAuthenticationRepository authRepo,
|
||||
IJsonSerializer json,
|
||||
ILibraryManager libraryManager,
|
||||
ILocalizationManager localizationManager,
|
||||
IUserManager userManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryMonitor libraryMonitor,
|
||||
IServerConfigurationManager config)
|
||||
{
|
||||
_json = json;
|
||||
_userManager = userManager;
|
||||
_fileSystem = fileSystem;
|
||||
_libraryMonitor = libraryMonitor;
|
||||
_config = config;
|
||||
_libraryManager = libraryManager;
|
||||
_localizationManager = localizationManager;
|
||||
_authRepo = authRepo;
|
||||
_capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@ -194,172 +170,6 @@ namespace Emby.Server.Implementations.Devices
|
||||
return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public ContentUploadHistory GetCameraUploadHistory(string deviceId)
|
||||
{
|
||||
var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
|
||||
|
||||
lock (_cameraUploadSyncLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _json.DeserializeFromFile<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)
|
||||
{
|
||||
if (user == null)
|
||||
@ -399,102 +209,4 @@ namespace Emby.Server.Implementations.Devices
|
||||
return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public class DeviceManagerEntryPoint : IServerEntryPoint
|
||||
{
|
||||
private readonly DeviceManager _deviceManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private ILogger _logger;
|
||||
|
||||
public DeviceManagerEntryPoint(
|
||||
IDeviceManager deviceManager,
|
||||
IServerConfigurationManager config,
|
||||
ILogger<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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
@ -34,15 +34,16 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" 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.Extensions.DependencyInjection" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" />
|
||||
<PackageReference Include="Mono.Nat" Version="2.0.1" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.25.0" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -210,16 +210,8 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog)
|
||||
private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
|
||||
{
|
||||
bool ignoreStackTrace =
|
||||
ex is SocketException
|
||||
|| ex is IOException
|
||||
|| ex is OperationCanceledException
|
||||
|| ex is SecurityException
|
||||
|| ex is AuthenticationException
|
||||
|| ex is FileNotFoundException;
|
||||
|
||||
if (ignoreStackTrace)
|
||||
{
|
||||
_logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
|
||||
@ -505,14 +497,32 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
var requestInnerEx = GetActualException(requestEx);
|
||||
var statusCode = GetStatusCode(requestInnerEx);
|
||||
|
||||
// Do not handle 500 server exceptions manually when in development mode
|
||||
// The framework-defined development exception page will be returned instead
|
||||
if (statusCode == 500 && _hostEnvironment.IsDevelopment())
|
||||
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
{
|
||||
if (!httpRes.Headers.ContainsKey(key))
|
||||
{
|
||||
httpRes.Headers.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
bool ignoreStackTrace =
|
||||
requestInnerEx is SocketException
|
||||
|| requestInnerEx is IOException
|
||||
|| requestInnerEx is OperationCanceledException
|
||||
|| requestInnerEx is SecurityException
|
||||
|| requestInnerEx is AuthenticationException
|
||||
|| requestInnerEx is FileNotFoundException;
|
||||
|
||||
// Do not handle 500 server exceptions manually when in development mode.
|
||||
// Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware.
|
||||
// However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored,
|
||||
// because it will log the stack trace when it handles the exception.
|
||||
if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment())
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog).ConfigureAwait(false);
|
||||
await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception handlerException)
|
||||
{
|
||||
|
@ -11,6 +11,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
@ -37,38 +38,6 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
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>
|
||||
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
|
||||
/// </summary>
|
||||
@ -395,12 +364,7 @@ namespace Emby.Server.Implementations.IO
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
var filename = Path.GetFileName(path);
|
||||
|
||||
var monitorPath = !string.IsNullOrEmpty(filename) &&
|
||||
!_alwaysIgnoreFiles.Contains(filename) &&
|
||||
!_alwaysIgnoreExtensions.Contains(Path.GetExtension(path)) &&
|
||||
_alwaysIgnoreSubstrings.All(i => path.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1);
|
||||
var monitorPath = !IgnorePatterns.ShouldIgnore(path);
|
||||
|
||||
// Ignore certain files
|
||||
var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList();
|
||||
|
@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
@ -16,32 +14,6 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <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>
|
||||
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
|
||||
/// </summary>
|
||||
@ -60,23 +32,15 @@ namespace Emby.Server.Implementations.Library
|
||||
return false;
|
||||
}
|
||||
|
||||
var filename = fileInfo.Name;
|
||||
|
||||
// Ignore hidden files on UNIX
|
||||
if (Environment.OSVersion.Platform != PlatformID.Win32NT
|
||||
&& filename[0] == '.')
|
||||
if (IgnorePatterns.ShouldIgnore(fileInfo.FullName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var filename = fileInfo.Name;
|
||||
|
||||
if (fileInfo.IsDirectory)
|
||||
{
|
||||
// Ignore any folders in our list
|
||||
if (_ignoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parent != null)
|
||||
{
|
||||
// Ignore trailer folders but allow it at the collection level
|
||||
@ -109,11 +73,6 @@ namespace Emby.Server.Implementations.Library
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore samples
|
||||
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
|
||||
|
||||
return m.Success;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
74
Emby.Server.Implementations/Library/IgnorePatterns.cs
Normal file
74
Emby.Server.Implementations/Library/IgnorePatterns.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
@ -118,6 +119,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
//OpenedMediaSource.SupportsDirectStream = true;
|
||||
//OpenedMediaSource.SupportsTranscoding = true;
|
||||
await taskCompletionSource.Task.ConfigureAwait(false);
|
||||
if (taskCompletionSource.Task.Exception != null)
|
||||
{
|
||||
// Error happened while opening the stream so raise the exception again to inform the caller
|
||||
throw taskCompletionSource.Task.Exception;
|
||||
}
|
||||
|
||||
if (!taskCompletionSource.Task.Result)
|
||||
{
|
||||
Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
|
||||
throw new EndOfStreamException(String.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
|
||||
}
|
||||
}
|
||||
|
||||
private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
@ -139,14 +151,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath);
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error copying live stream.");
|
||||
Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath);
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
|
||||
openTaskCompletionSource.TrySetResult(false);
|
||||
|
||||
EnableStreamSharing = false;
|
||||
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
|
||||
});
|
||||
|
@ -9,7 +9,7 @@
|
||||
"Channels": "القنوات",
|
||||
"ChapterNameValue": "الفصل {0}",
|
||||
"Collections": "مجموعات",
|
||||
"DeviceOfflineWithName": "قُطِع الاتصال بـ{0}",
|
||||
"DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
|
||||
"DeviceOnlineWithName": "{0} متصل",
|
||||
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
|
||||
"Favorites": "المفضلة",
|
||||
|
@ -23,7 +23,7 @@
|
||||
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
||||
"HeaderFavoriteShows": "Oblíbené seriály",
|
||||
"HeaderFavoriteSongs": "Oblíbená hudba",
|
||||
"HeaderLiveTV": "Živá TV",
|
||||
"HeaderLiveTV": "Televize",
|
||||
"HeaderNextUp": "Nadcházející",
|
||||
"HeaderRecordingGroups": "Skupiny nahrávek",
|
||||
"HomeVideos": "Domáci videa",
|
||||
|
@ -24,7 +24,7 @@
|
||||
"HeaderFavoriteShows": "Programas favoritos",
|
||||
"HeaderFavoriteSongs": "Canciones favoritas",
|
||||
"HeaderLiveTV": "TV en vivo",
|
||||
"HeaderNextUp": "A Continuación",
|
||||
"HeaderNextUp": "Siguiente",
|
||||
"HeaderRecordingGroups": "Grupos de grabación",
|
||||
"HomeVideos": "Videos caseros",
|
||||
"Inherit": "Heredar",
|
||||
@ -44,7 +44,7 @@
|
||||
"NameInstallFailed": "{0} instalación fallida",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameSeasonUnknown": "Temporada desconocida",
|
||||
"NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.",
|
||||
"NewVersionIsAvailable": "Una nueva versión del servidor Jellyfin está disponible para descargar.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada",
|
||||
"NotificationOptionAudioPlayback": "Se inició la reproducción de audio",
|
||||
@ -56,7 +56,7 @@
|
||||
"NotificationOptionPluginInstalled": "Complemento instalado",
|
||||
"NotificationOptionPluginUninstalled": "Complemento desinstalado",
|
||||
"NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada",
|
||||
"NotificationOptionServerRestartRequired": "Se necesita reiniciar el Servidor",
|
||||
"NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
|
||||
"NotificationOptionTaskFailed": "Falla de tarea programada",
|
||||
"NotificationOptionUserLockedOut": "Usuario bloqueado",
|
||||
"NotificationOptionVideoPlayback": "Se inició la reproducción de video",
|
||||
@ -71,7 +71,7 @@
|
||||
"ScheduledTaskFailedWithName": "{0} falló",
|
||||
"ScheduledTaskStartedWithName": "{0} iniciado",
|
||||
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
|
||||
"Shows": "Series",
|
||||
"Shows": "Programas",
|
||||
"Songs": "Canciones",
|
||||
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
@ -94,25 +94,25 @@
|
||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||
"VersionNumber": "Versión {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten basándose en la configuración de los metadatos.",
|
||||
"TaskDownloadMissingSubtitles": "Descargar subtítulos extraviados",
|
||||
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
|
||||
"TaskRefreshChannelsDescription": "Actualizar información de canales de internet.",
|
||||
"TaskRefreshChannels": "Actualizar canales",
|
||||
"TaskCleanTranscodeDescription": "Eliminar archivos transcodificados con mas de un día de antigüedad.",
|
||||
"TaskCleanTranscode": "Limpiar directorio de Transcodificado",
|
||||
"TaskCleanTranscode": "Limpiar directorio de transcodificación",
|
||||
"TaskUpdatePluginsDescription": "Descargar e instalar actualizaciones para complementos que estén configurados en actualizar automáticamente.",
|
||||
"TaskUpdatePlugins": "Actualizar complementos",
|
||||
"TaskRefreshPeopleDescription": "Actualizar metadatos de actores y directores en su librería multimedia.",
|
||||
"TaskRefreshPeopleDescription": "Actualizar metadatos de actores y directores en su biblioteca multimedia.",
|
||||
"TaskRefreshPeople": "Actualizar personas",
|
||||
"TaskCleanLogsDescription": "Eliminar archivos de registro que tengan mas de {0} días de antigüedad.",
|
||||
"TaskCleanLogs": "Limpiar directorio de registros",
|
||||
"TaskRefreshLibraryDescription": "Escanear su librería multimedia por nuevos archivos y refrescar metadatos.",
|
||||
"TaskRefreshLibrary": "Escanear librería multimedia",
|
||||
"TaskRefreshLibraryDescription": "Escanear su biblioteca multimedia por nuevos archivos y refrescar metadatos.",
|
||||
"TaskRefreshLibrary": "Escanear biblioteca multimedia",
|
||||
"TaskRefreshChapterImagesDescription": "Crear miniaturas de videos que tengan capítulos.",
|
||||
"TaskRefreshChapterImages": "Extraer imágenes de capitulo",
|
||||
"TaskCleanCacheDescription": "Eliminar archivos de cache que no se necesiten en el sistema.",
|
||||
"TaskCleanCache": "Limpiar directorio Cache",
|
||||
"TasksChannelsCategory": "Canales de Internet",
|
||||
"TasksApplicationCategory": "Solicitud",
|
||||
"TaskRefreshChapterImages": "Extraer imágenes de capítulo",
|
||||
"TaskCleanCacheDescription": "Eliminar archivos de caché que no se necesiten en el sistema.",
|
||||
"TaskCleanCache": "Limpiar directorio caché",
|
||||
"TasksChannelsCategory": "Canales de internet",
|
||||
"TasksApplicationCategory": "Aplicación",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksMaintenanceCategory": "Mantenimiento"
|
||||
}
|
||||
|
@ -16,16 +16,16 @@
|
||||
"Folders": "Carpetas",
|
||||
"Genres": "Géneros",
|
||||
"HeaderAlbumArtists": "Artistas del álbum",
|
||||
"HeaderCameraUploads": "Subidos desde Camara",
|
||||
"HeaderContinueWatching": "Continuar Viendo",
|
||||
"HeaderCameraUploads": "Subidas desde la cámara",
|
||||
"HeaderContinueWatching": "Continuar viendo",
|
||||
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||
"HeaderFavoriteEpisodes": "Episodios favoritos",
|
||||
"HeaderFavoriteShows": "Programas favoritos",
|
||||
"HeaderFavoriteSongs": "Canciones favoritas",
|
||||
"HeaderLiveTV": "TV en Vivo",
|
||||
"HeaderNextUp": "A Continuación",
|
||||
"HeaderRecordingGroups": "Grupos de Grabaciones",
|
||||
"HeaderLiveTV": "TV en vivo",
|
||||
"HeaderNextUp": "A continuación",
|
||||
"HeaderRecordingGroups": "Grupos de grabación",
|
||||
"HomeVideos": "Videos caseros",
|
||||
"Inherit": "Heredar",
|
||||
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
|
||||
@ -41,12 +41,12 @@
|
||||
"Movies": "Películas",
|
||||
"Music": "Música",
|
||||
"MusicVideos": "Videos musicales",
|
||||
"NameInstallFailed": "{0} instalación fallida",
|
||||
"NameInstallFailed": "Falló la instalación de {0}",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameSeasonUnknown": "Temporada Desconocida",
|
||||
"NameSeasonUnknown": "Temporada desconocida",
|
||||
"NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Actualización de aplicación disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Actualización de aplicación instalada",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada",
|
||||
"NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
|
||||
"NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
|
||||
@ -56,7 +56,7 @@
|
||||
"NotificationOptionPluginInstalled": "Complemento instalado",
|
||||
"NotificationOptionPluginUninstalled": "Complemento desinstalado",
|
||||
"NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada",
|
||||
"NotificationOptionServerRestartRequired": "Se necesita reiniciar el Servidor",
|
||||
"NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
|
||||
"NotificationOptionTaskFailed": "Falla de tarea programada",
|
||||
"NotificationOptionUserLockedOut": "Usuario bloqueado",
|
||||
"NotificationOptionVideoPlayback": "Reproducción de video iniciada",
|
||||
@ -69,48 +69,48 @@
|
||||
"PluginUpdatedWithName": "{0} fue actualizado",
|
||||
"ProviderValue": "Proveedor: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} falló",
|
||||
"ScheduledTaskStartedWithName": "{0} Iniciado",
|
||||
"ScheduledTaskStartedWithName": "{0} iniciado",
|
||||
"ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
|
||||
"Shows": "Programas",
|
||||
"Songs": "Canciones",
|
||||
"StartupEmbyServerIsLoading": "El servidor Jellyfin esta cargando. Por favor intente de nuevo dentro de poco.",
|
||||
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
|
||||
"SubtitleDownloadFailureForItem": "Falló la descarga de subtítulos para {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
|
||||
"Sync": "Sincronizar",
|
||||
"System": "Sistema",
|
||||
"TvShows": "Programas de TV",
|
||||
"User": "Usuario",
|
||||
"UserCreatedWithName": "Se ha creado el usuario {0}",
|
||||
"UserDeletedWithName": "Se ha eliminado el usuario {0}",
|
||||
"UserDownloadingItemWithValues": "{0} esta descargando {1}",
|
||||
"UserCreatedWithName": "El usuario {0} ha sido creado",
|
||||
"UserDeletedWithName": "El usuario {0} ha sido eliminado",
|
||||
"UserDownloadingItemWithValues": "{0} está descargando {1}",
|
||||
"UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
|
||||
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
|
||||
"UserOnlineFromDevice": "{0} está en línea desde {1}",
|
||||
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
|
||||
"UserPolicyUpdatedWithName": "Las política de usuario ha sido actualizada por {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} está reproduciéndose {1} en {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducirse {1} en {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} se han añadido a su biblioteca de medios",
|
||||
"UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca de medios",
|
||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||
"VersionNumber": "Versión {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Buscar subtítulos de internet basado en configuración de metadatos.",
|
||||
"TaskDownloadMissingSubtitles": "Descargar subtítulos perdidos",
|
||||
"TaskRefreshChannelsDescription": "Refrescar información de canales de internet.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
|
||||
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
|
||||
"TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
|
||||
"TaskRefreshChannels": "Actualizar canales",
|
||||
"TaskCleanTranscodeDescription": "Eliminar archivos transcodificados que tengan mas de un día.",
|
||||
"TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
|
||||
"TaskCleanTranscode": "Limpiar directorio de transcodificado",
|
||||
"TaskUpdatePluginsDescription": "Descargar y actualizar complementos que están configurados para actualizarse automáticamente.",
|
||||
"TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
|
||||
"TaskUpdatePlugins": "Actualizar complementos",
|
||||
"TaskRefreshPeopleDescription": "Actualizar datos de actores y directores en su librería multimedia.",
|
||||
"TaskRefreshPeople": "Refrescar persona",
|
||||
"TaskCleanLogsDescription": "Eliminar archivos de registro con mas de {0} días.",
|
||||
"TaskCleanLogs": "Directorio de logo limpio",
|
||||
"TaskRefreshLibraryDescription": "Escanear su librería multimedia para nuevos archivos y refrescar metadatos.",
|
||||
"TaskRefreshLibrary": "Escanear librería multimerdia",
|
||||
"TaskRefreshChapterImagesDescription": "Crear miniaturas para videos con capítulos.",
|
||||
"TaskRefreshChapterImages": "Extraer imágenes de capítulos",
|
||||
"TaskCleanCacheDescription": "Eliminar archivos cache que ya no se necesiten por el sistema.",
|
||||
"TaskCleanCache": "Limpiar directorio cache",
|
||||
"TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
|
||||
"TaskRefreshPeople": "Actualizar personas",
|
||||
"TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad.",
|
||||
"TaskCleanLogs": "Limpiar directorio de registros",
|
||||
"TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios por archivos nuevos y actualiza los metadatos.",
|
||||
"TaskRefreshLibrary": "Escanear biblioteca de medios",
|
||||
"TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
|
||||
"TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
|
||||
"TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.",
|
||||
"TaskCleanCache": "Limpiar directorio caché",
|
||||
"TasksChannelsCategory": "Canales de Internet",
|
||||
"TasksApplicationCategory": "Aplicación",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
|
@ -96,21 +96,22 @@
|
||||
"TasksLibraryCategory": "Bibliothèque",
|
||||
"TasksMaintenanceCategory": "Entretien",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.",
|
||||
"TaskDownloadMissingSubtitles": "Télécharger des sous-titres manquants",
|
||||
"TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines d'internet.",
|
||||
"TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants",
|
||||
"TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines internet.",
|
||||
"TaskRefreshChannels": "Rafraîchir des chaines",
|
||||
"TaskCleanTranscodeDescription": "Retirer des fichiers de transcodage de plus qu'un jour.",
|
||||
"TaskCleanTranscode": "Nettoyer le directoire de transcodage",
|
||||
"TaskUpdatePluginsDescription": "Télécharger et installer des mises à jours des plugins qui sont configurés m.à.j. automisés.",
|
||||
"TaskUpdatePlugins": "Mise à jour des plugins",
|
||||
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.",
|
||||
"TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage de plus d'un jour.",
|
||||
"TaskCleanTranscode": "Nettoyer le répertoire de transcodage",
|
||||
"TaskUpdatePluginsDescription": "Télécharger et installer les mises à jours des extensions qui sont configurés pour les m.à.j. automisés.",
|
||||
"TaskUpdatePlugins": "Mise à jour des extensions",
|
||||
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque de médias.",
|
||||
"TaskRefreshPeople": "Rafraîchir les acteurs",
|
||||
"TaskCleanLogsDescription": "Retire les données qui ont plus que {0} jours.",
|
||||
"TaskCleanLogs": "Nettoyer les données de directoire",
|
||||
"TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour des nouveaux fichiers et rafraîchit les métadonnées.",
|
||||
"TaskRefreshChapterImages": "Extraire des images du chapitre",
|
||||
"TaskRefreshChapterImagesDescription": "Créer des vignettes pour des vidéos qui ont des chapitres",
|
||||
"TaskRefreshLibrary": "Analyser la bibliothèque de média",
|
||||
"TaskCleanCache": "Nettoyer le cache de directoire",
|
||||
"TasksApplicationCategory": "Application"
|
||||
"TaskCleanLogsDescription": "Supprime les journaux qui ont plus que {0} jours.",
|
||||
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
|
||||
"TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour trouver de nouveaux fichiers et rafraîchit les métadonnées.",
|
||||
"TaskRefreshChapterImages": "Extraire les images de chapitre",
|
||||
"TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres",
|
||||
"TaskRefreshLibrary": "Analyser la bibliothèque de médias",
|
||||
"TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires",
|
||||
"TasksApplicationCategory": "Application",
|
||||
"TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système."
|
||||
}
|
||||
|
@ -107,5 +107,12 @@
|
||||
"TaskCleanLogs": "נקה תיקיית יומן",
|
||||
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
|
||||
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
|
||||
"TasksChannelsCategory": "ערוצי אינטרנט"
|
||||
"TasksChannelsCategory": "ערוצי אינטרנט",
|
||||
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
|
||||
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.",
|
||||
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
|
||||
"TaskRefreshChannels": "רענן ערוץ",
|
||||
"TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",
|
||||
"TaskCleanTranscode": "נקה תקיית Transcode",
|
||||
"TaskUpdatePluginsDescription": "הורד והתקן עדכונים עבור תוספים שמוגדרים לעדכון אוטומטי."
|
||||
}
|
||||
|
@ -80,16 +80,32 @@
|
||||
"ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
|
||||
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} er að spila {1} á {2}",
|
||||
"UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir notanda {0}",
|
||||
"UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir {0}",
|
||||
"UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
|
||||
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
|
||||
"UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
|
||||
"UserLockedOutWithName": "Notanda {0} hefur verið hindraður aðgangur",
|
||||
"UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur",
|
||||
"UserDownloadingItemWithValues": "{0} Hleður niður {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
|
||||
"ProviderValue": "Veitandi: {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
|
||||
"ValueSpecialEpisodeName": "Sérstakt - {0}",
|
||||
"Shows": "Þættir",
|
||||
"Playlists": "Spilunarlisti"
|
||||
"Shows": "Sýningar",
|
||||
"Playlists": "Spilunarlisti",
|
||||
"TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
|
||||
"TaskRefreshChannels": "Endurhlaða Rásir",
|
||||
"TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
|
||||
"TaskCleanTranscode": "Hreinsa Umkóðunarmöppu",
|
||||
"TaskUpdatePluginsDescription": "Sækja og setja upp uppfærslur fyrir viðbætur sem eru stilltar til að uppfæra sjálfkrafa.",
|
||||
"TaskUpdatePlugins": "Uppfæra viðbætur",
|
||||
"TaskRefreshPeopleDescription": "Uppfærir lýsigögn fyrir leikara og leikstjóra í miðlasafninu þínu.",
|
||||
"TaskRefreshLibraryDescription": "Skannar miðlasafnið þitt fyrir nýjum skrám og uppfærir lýsigögn.",
|
||||
"TaskRefreshLibrary": "Skanna miðlasafn",
|
||||
"TaskRefreshChapterImagesDescription": "Býr til smámyndir fyrir myndbönd sem hafa kaflaskil.",
|
||||
"TaskCleanCacheDescription": "Eyðir skrám í skyndiminni sem ekki er lengur þörf fyrir í kerfinu.",
|
||||
"TaskCleanCache": "Hreinsa skráasafn skyndiminnis",
|
||||
"TasksChannelsCategory": "Netrásir",
|
||||
"TasksApplicationCategory": "Forrit",
|
||||
"TasksLibraryCategory": "Miðlasafn",
|
||||
"TasksMaintenanceCategory": "Viðhald"
|
||||
}
|
||||
|
@ -92,5 +92,27 @@
|
||||
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką",
|
||||
"ValueSpecialEpisodeName": "Ypatinga - {0}",
|
||||
"VersionNumber": "Version {0}"
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.",
|
||||
"TaskUpdatePlugins": "Atnaujinti Priedus",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Ieško internete trūkstamų subtitrų remiantis metaduomenų konfigūracija.",
|
||||
"TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
|
||||
"TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija",
|
||||
"TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.",
|
||||
"TaskRefreshLibrary": "Skenuoti Mediateka",
|
||||
"TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
|
||||
"TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informacija.",
|
||||
"TaskRefreshChannels": "Atnaujinti Kanalus",
|
||||
"TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.",
|
||||
"TaskRefreshPeople": "Atnaujinti Žmones",
|
||||
"TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
|
||||
"TaskCleanLogs": "Išvalyti Žurnalą",
|
||||
"TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašam, kurie turi scenas.",
|
||||
"TaskRefreshChapterImages": "Ištraukti Scenų Paveikslus",
|
||||
"TaskCleanCache": "Išvalyti Talpyklą",
|
||||
"TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.",
|
||||
"TasksChannelsCategory": "Internetiniai Kanalai",
|
||||
"TasksApplicationCategory": "Programa",
|
||||
"TasksLibraryCategory": "Mediateka",
|
||||
"TasksMaintenanceCategory": "Priežiūra"
|
||||
}
|
||||
|
@ -19,10 +19,10 @@
|
||||
"HeaderCameraUploads": "Envios da Câmera",
|
||||
"HeaderContinueWatching": "Continuar Assistindo",
|
||||
"HeaderFavoriteAlbums": "Álbuns Favoritos",
|
||||
"HeaderFavoriteArtists": "Artistas Favoritos",
|
||||
"HeaderFavoriteEpisodes": "Episódios Favoritos",
|
||||
"HeaderFavoriteShows": "Séries Favoritas",
|
||||
"HeaderFavoriteSongs": "Músicas Favoritas",
|
||||
"HeaderFavoriteArtists": "Artistas favoritos",
|
||||
"HeaderFavoriteEpisodes": "Episódios favoritos",
|
||||
"HeaderFavoriteShows": "Séries favoritas",
|
||||
"HeaderFavoriteSongs": "Músicas favoritas",
|
||||
"HeaderLiveTV": "TV ao Vivo",
|
||||
"HeaderNextUp": "A Seguir",
|
||||
"HeaderRecordingGroups": "Grupos de Gravação",
|
||||
|
71
Emby.Server.Implementations/Localization/Core/th.json
Normal file
71
Emby.Server.Implementations/Localization/Core/th.json
Normal 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": "อัลบั้ม"
|
||||
}
|
@ -11,15 +11,15 @@
|
||||
"Collections": "合輯",
|
||||
"DeviceOfflineWithName": "{0} 已經斷開連結",
|
||||
"DeviceOnlineWithName": "{0} 已經連接",
|
||||
"FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試",
|
||||
"FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗",
|
||||
"Favorites": "我的最愛",
|
||||
"Folders": "檔案夾",
|
||||
"Genres": "風格",
|
||||
"HeaderAlbumArtists": "專輯藝術家",
|
||||
"HeaderAlbumArtists": "專輯藝人",
|
||||
"HeaderCameraUploads": "相機上載",
|
||||
"HeaderContinueWatching": "繼續觀看",
|
||||
"HeaderFavoriteAlbums": "最愛專輯",
|
||||
"HeaderFavoriteArtists": "最愛藝術家",
|
||||
"HeaderFavoriteArtists": "最愛的藝人",
|
||||
"HeaderFavoriteEpisodes": "最愛的劇集",
|
||||
"HeaderFavoriteShows": "最愛的節目",
|
||||
"HeaderFavoriteSongs": "最愛的歌曲",
|
||||
@ -33,14 +33,14 @@
|
||||
"LabelIpAddressValue": "IP 地址: {0}",
|
||||
"LabelRunningTimeValue": "運行時間: {0}",
|
||||
"Latest": "最新",
|
||||
"MessageApplicationUpdated": "Jellyfin Server 已更新",
|
||||
"MessageApplicationUpdated": "Jellyfin 伺服器已更新",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin 伺服器已更新至 {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已更新",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已更新",
|
||||
"MessageServerConfigurationUpdated": "伺服器設定已經更新",
|
||||
"MixedContent": "Mixed content",
|
||||
"MixedContent": "混合內容",
|
||||
"Movies": "電影",
|
||||
"Music": "音樂",
|
||||
"MusicVideos": "音樂MV",
|
||||
"MusicVideos": "音樂視頻",
|
||||
"NameInstallFailed": "{0} 安裝失敗",
|
||||
"NameSeasonNumber": "第 {0} 季",
|
||||
"NameSeasonUnknown": "未知季數",
|
||||
@ -49,7 +49,7 @@
|
||||
"NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
|
||||
"NotificationOptionAudioPlayback": "開始播放音頻",
|
||||
"NotificationOptionAudioPlaybackStopped": "已停止播放音頻",
|
||||
"NotificationOptionCameraImageUploaded": "相機相片已上傳",
|
||||
"NotificationOptionCameraImageUploaded": "相片已上傳",
|
||||
"NotificationOptionInstallationFailed": "安裝失敗",
|
||||
"NotificationOptionNewLibraryContent": "已添加新内容",
|
||||
"NotificationOptionPluginError": "擴充元件錯誤",
|
||||
@ -63,11 +63,11 @@
|
||||
"NotificationOptionVideoPlaybackStopped": "已停止播放視頻",
|
||||
"Photos": "相片",
|
||||
"Playlists": "播放清單",
|
||||
"Plugin": "Plugin",
|
||||
"Plugin": "插件",
|
||||
"PluginInstalledWithName": "已安裝 {0}",
|
||||
"PluginUninstalledWithName": "已移除 {0}",
|
||||
"PluginUpdatedWithName": "已更新 {0}",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ProviderValue": "提供者: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} 任務失敗",
|
||||
"ScheduledTaskStartedWithName": "{0} 任務開始",
|
||||
"ServerNameNeedsToBeRestarted": "{0} 需要重啓",
|
||||
@ -77,17 +77,17 @@
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕",
|
||||
"Sync": "同步",
|
||||
"System": "System",
|
||||
"System": "系統",
|
||||
"TvShows": "電視節目",
|
||||
"User": "User",
|
||||
"UserCreatedWithName": "用家 {0} 已創建",
|
||||
"UserDeletedWithName": "用家 {0} 已移除",
|
||||
"User": "使用者",
|
||||
"UserCreatedWithName": "使用者 {0} 已創建",
|
||||
"UserDeletedWithName": "使用者 {0} 已移除",
|
||||
"UserDownloadingItemWithValues": "{0} 正在下載 {1}",
|
||||
"UserLockedOutWithName": "用家 {0} 已被鎖定",
|
||||
"UserLockedOutWithName": "使用者 {0} 已被鎖定",
|
||||
"UserOfflineFromDevice": "{0} 已從 {1} 斷開",
|
||||
"UserOnlineFromDevice": "{0} 已連綫,來自 {1}",
|
||||
"UserPasswordChangedWithName": "用家 {0} 的密碼已變更",
|
||||
"UserPolicyUpdatedWithName": "用戶協議已被更新為 {0}",
|
||||
"UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
|
||||
"UserPolicyUpdatedWithName": "使用者協議已更新為 {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫",
|
||||
@ -95,5 +95,23 @@
|
||||
"VersionNumber": "版本{0}",
|
||||
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
|
||||
"TaskUpdatePlugins": "更新插件",
|
||||
"TasksApplicationCategory": "應用程式"
|
||||
"TasksApplicationCategory": "應用程式",
|
||||
"TaskRefreshLibraryDescription": "掃描媒體庫以查找新文件並刷新metadata。",
|
||||
"TasksMaintenanceCategory": "維護",
|
||||
"TaskDownloadMissingSubtitlesDescription": "根據metadata配置在互聯網上搜索缺少的字幕。",
|
||||
"TaskRefreshChannelsDescription": "刷新互聯網頻道信息。",
|
||||
"TaskRefreshChannels": "刷新頻道",
|
||||
"TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。",
|
||||
"TaskCleanTranscode": "清理轉碼目錄",
|
||||
"TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。",
|
||||
"TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的metadata。",
|
||||
"TaskCleanLogsDescription": "刪除超過{0}天的日誌文件。",
|
||||
"TaskCleanLogs": "清理日誌目錄",
|
||||
"TaskRefreshLibrary": "掃描媒體庫",
|
||||
"TaskRefreshChapterImagesDescription": "為帶有章節的視頻創建縮略圖。",
|
||||
"TaskRefreshChapterImages": "提取章節圖像",
|
||||
"TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。",
|
||||
"TaskCleanCache": "清理緩存目錄",
|
||||
"TasksChannelsCategory": "互聯網頻道",
|
||||
"TasksLibraryCategory": "庫"
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
@ -59,6 +60,10 @@ namespace Jellyfin.Api.Auth
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
catch (AuthenticationException ex)
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail(ex));
|
||||
}
|
||||
catch (SecurityException ex)
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail(ex));
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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="Swashbuckle.AspNetCore" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
154
Jellyfin.Data/Entities/ActivityLog.cs
Normal file
154
Jellyfin.Data/Entities/ActivityLog.cs
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</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>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="2.2.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
102
Jellyfin.Server.Implementations/Activity/ActivityManager.cs
Normal file
102
Jellyfin.Server.Implementations/Activity/ActivityManager.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -25,6 +25,17 @@
|
||||
<Compile Remove="Migrations\20200430214405_InitialSchema.Designer.cs" />
|
||||
</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>
|
||||
<ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
|
@ -15,6 +15,7 @@ namespace Jellyfin.Server.Implementations
|
||||
/// <inheritdoc/>
|
||||
public partial class JellyfinDb : DbContext
|
||||
{
|
||||
public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
|
||||
/*public virtual DbSet<Artwork> Artwork { get; set; }
|
||||
public virtual DbSet<Book> Books { 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<ProviderMapping> ProviderMappings { get; set; }
|
||||
public virtual DbSet<Rating> Ratings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
|
||||
/// store review ratings, not age ratings
|
||||
@ -93,8 +95,10 @@ namespace Jellyfin.Server.Implementations
|
||||
modelBuilder.HasDefaultSchema("jellyfin");
|
||||
|
||||
/*modelBuilder.Entity<Artwork>().HasIndex(t => t.Kind);
|
||||
|
||||
modelBuilder.Entity<Genre>().HasIndex(t => t.Name)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<LibraryItem>().HasIndex(t => t.UrlId)
|
||||
.IsUnique();*/
|
||||
|
||||
@ -103,9 +107,10 @@ namespace Jellyfin.Server.Implementations
|
||||
|
||||
public override int SaveChanges()
|
||||
{
|
||||
foreach (var entity in ChangeTracker.Entries().Where(e => e.State == EntityState.Modified))
|
||||
foreach (var saveEntity in ChangeTracker.Entries()
|
||||
.Where(e => e.State == EntityState.Modified)
|
||||
.OfType<ISavingChanges>())
|
||||
{
|
||||
var saveEntity = entity.Entity as ISavingChanges;
|
||||
saveEntity.OnSavingChanges();
|
||||
}
|
||||
|
||||
|
33
Jellyfin.Server.Implementations/JellyfinDbProvider.cs
Normal file
33
Jellyfin.Server.Implementations/JellyfinDbProvider.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
72
Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs
generated
Normal file
72
Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Emby.Drawing;
|
||||
using Emby.Server.Implementations;
|
||||
using Jellyfin.Drawing.Skia;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using Jellyfin.Server.Implementations.Activity;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -56,6 +61,15 @@ namespace Jellyfin.Server
|
||||
Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
|
||||
}
|
||||
|
||||
// TODO: Set up scoping and use AddDbContextPool
|
||||
serviceCollection.AddDbContext<JellyfinDb>(
|
||||
options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
|
||||
ServiceLifetime.Transient);
|
||||
|
||||
serviceCollection.AddSingleton<JellyfinDbProvider>();
|
||||
|
||||
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
|
||||
|
||||
base.RegisterServices(serviceCollection);
|
||||
}
|
||||
|
||||
|
@ -41,8 +41,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.7.82" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.4" />
|
||||
<PackageReference Include="prometheus-net" Version="3.5.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="3.5.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
|
||||
@ -60,6 +60,7 @@
|
||||
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
|
||||
<ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" />
|
||||
<ProjectReference Include="..\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" />
|
||||
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -17,7 +17,9 @@ namespace Jellyfin.Server.Migrations
|
||||
private static readonly Type[] _migrationTypes =
|
||||
{
|
||||
typeof(Routines.DisableTranscodingThrottling),
|
||||
typeof(Routines.CreateUserLoggingConfigFile)
|
||||
typeof(Routines.CreateUserLoggingConfigFile),
|
||||
typeof(Routines.MigrateActivityLogDb),
|
||||
typeof(Routines.RemoveDuplicateExtras)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
133
Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
Normal file
133
Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs
Normal 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'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
79
Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
Normal file
79
Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
Normal 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')");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Net;
|
||||
@ -116,11 +115,6 @@ namespace MediaBrowser.Api.Devices
|
||||
return _deviceManager.GetDeviceOptions(request.Id);
|
||||
}
|
||||
|
||||
public object Get(GetCameraUploads request)
|
||||
{
|
||||
return ToOptimizedResult(_deviceManager.GetCameraUploadHistory(request.DeviceId));
|
||||
}
|
||||
|
||||
public void Delete(DeleteDevice request)
|
||||
{
|
||||
var sessions = _authRepo.Get(new AuthenticationInfoQuery
|
||||
@ -134,35 +128,5 @@ namespace MediaBrowser.Api.Devices
|
||||
_sessionManager.Logout(session);
|
||||
}
|
||||
}
|
||||
|
||||
public Task Post(PostCameraUpload request)
|
||||
{
|
||||
var deviceId = Request.QueryString["DeviceId"];
|
||||
var album = Request.QueryString["Album"];
|
||||
var id = Request.QueryString["Id"];
|
||||
var name = Request.QueryString["Name"];
|
||||
var req = Request.Response.HttpContext.Request;
|
||||
|
||||
if (req.HasFormContentType)
|
||||
{
|
||||
var file = req.Form.Files.Count == 0 ? null : req.Form.Files[0];
|
||||
|
||||
return _deviceManager.AcceptCameraUpload(deviceId, file.OpenReadStream(), new LocalFileInfo
|
||||
{
|
||||
MimeType = file.ContentType,
|
||||
Album = album,
|
||||
Name = name,
|
||||
Id = id
|
||||
});
|
||||
}
|
||||
|
||||
return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo
|
||||
{
|
||||
MimeType = Request.ContentType,
|
||||
Album = album,
|
||||
Name = name,
|
||||
Id = id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -763,13 +763,12 @@ namespace MediaBrowser.Api.Library
|
||||
{
|
||||
try
|
||||
{
|
||||
_activityManager.Create(new ActivityLogEntry
|
||||
_activityManager.Create(new Jellyfin.Data.Entities.ActivityLog(
|
||||
string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name),
|
||||
"UserDownloadingContent",
|
||||
auth.UserId)
|
||||
{
|
||||
Name = string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name),
|
||||
Type = "UserDownloadingContent",
|
||||
ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
|
||||
UserId = auth.UserId
|
||||
|
||||
});
|
||||
}
|
||||
catch
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Activity;
|
||||
@ -53,7 +55,10 @@ namespace MediaBrowser.Api.System
|
||||
(DateTime?)null :
|
||||
DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime();
|
||||
|
||||
var result = _activityManager.GetActivityLogEntries(minDate, request.HasUserId, request.StartIndex, request.Limit);
|
||||
var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
|
||||
entries => entries.Where(entry => entry.DateCreated >= minDate));
|
||||
|
||||
var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
@ -17,8 +17,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Devices;
|
||||
using MediaBrowser.Model.Events;
|
||||
@ -11,11 +9,6 @@ namespace MediaBrowser.Controller.Devices
|
||||
{
|
||||
public interface IDeviceManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when [camera image uploaded].
|
||||
/// </summary>
|
||||
event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
|
||||
|
||||
/// <summary>
|
||||
/// Saves the capabilities.
|
||||
/// </summary>
|
||||
@ -45,22 +38,6 @@ namespace MediaBrowser.Controller.Devices
|
||||
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
||||
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>
|
||||
/// Determines whether this instance [can access device] the specified user identifier.
|
||||
/// </summary>
|
||||
|
@ -13,8 +13,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -278,5 +278,19 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// <value>The disposition.</value>
|
||||
[JsonPropertyName("disposition")]
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
@ -695,6 +695,16 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
stream.RefFrames = streamInfo.Refs;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.ColorTransfer))
|
||||
{
|
||||
stream.ColorTransfer = streamInfo.ColorTransfer;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.ColorPrimaries))
|
||||
{
|
||||
stream.ColorPrimaries = streamInfo.ColorPrimaries;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -59,6 +59,7 @@ namespace MediaBrowser.Model.Activity
|
||||
/// Gets or sets the user primary image tag.
|
||||
/// </summary>
|
||||
/// <value>The user primary image tag.</value>
|
||||
[Obsolete("UserPrimaryImageTag is not used.")]
|
||||
public string UserPrimaryImageTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,6 +1,9 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
@ -10,10 +13,15 @@ namespace MediaBrowser.Model.Activity
|
||||
{
|
||||
event EventHandler<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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -79,8 +79,6 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
public bool EnableRemoteAccess { get; set; }
|
||||
|
||||
public bool CameraUploadUpgraded { get; set; }
|
||||
|
||||
public bool CollectionsUpgraded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
9
MediaBrowser.Model/Devices/DeviceOptions.cs
Normal file
9
MediaBrowser.Model/Devices/DeviceOptions.cs
Normal file
@ -0,0 +1,9 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Model.Devices
|
||||
{
|
||||
public class DeviceOptions
|
||||
{
|
||||
public string CustomName { get; set; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -34,8 +34,22 @@ namespace MediaBrowser.Model.Entities
|
||||
/// <value>The language.</value>
|
||||
public string Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color transfer.
|
||||
/// </summary>
|
||||
/// <value>The color transfer.</value>
|
||||
public string ColorTransfer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color primaries.
|
||||
/// </summary>
|
||||
/// <value>The color primaries.</value>
|
||||
public string ColorPrimaries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color space.
|
||||
/// </summary>
|
||||
/// <value>The color space.</value>
|
||||
public string ColorSpace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@ -44,11 +58,28 @@ namespace MediaBrowser.Model.Entities
|
||||
/// <value>The comment.</value>
|
||||
public string Comment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time base.
|
||||
/// </summary>
|
||||
/// <value>The time base.</value>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
/// <value>The title.</value>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video range.
|
||||
/// </summary>
|
||||
/// <value>The video range.</value>
|
||||
public string VideoRange
|
||||
{
|
||||
get
|
||||
@ -60,7 +91,8 @@ namespace MediaBrowser.Model.Entities
|
||||
|
||||
var colorTransfer = ColorTransfer;
|
||||
|
||||
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "HDR";
|
||||
}
|
||||
@ -70,7 +102,9 @@ namespace MediaBrowser.Model.Entities
|
||||
}
|
||||
|
||||
public string localizedUndefined { get; set; }
|
||||
|
||||
public string localizedDefault { get; set; }
|
||||
|
||||
public string localizedForced { get; set; }
|
||||
|
||||
public string DisplayTitle
|
||||
@ -197,34 +231,34 @@ namespace MediaBrowser.Model.Entities
|
||||
{
|
||||
if (i.IsInterlaced)
|
||||
{
|
||||
return "1440I";
|
||||
return "1440i";
|
||||
}
|
||||
return "1440P";
|
||||
return "1440p";
|
||||
}
|
||||
if (width >= 1900 || height >= 1000)
|
||||
{
|
||||
if (i.IsInterlaced)
|
||||
{
|
||||
return "1080I";
|
||||
return "1080i";
|
||||
}
|
||||
return "1080P";
|
||||
return "1080p";
|
||||
}
|
||||
if (width >= 1260 || height >= 700)
|
||||
{
|
||||
if (i.IsInterlaced)
|
||||
{
|
||||
return "720I";
|
||||
return "720i";
|
||||
}
|
||||
return "720P";
|
||||
return "720p";
|
||||
}
|
||||
if (width >= 700 || height >= 440)
|
||||
{
|
||||
|
||||
if (i.IsInterlaced)
|
||||
{
|
||||
return "480I";
|
||||
return "480i";
|
||||
}
|
||||
return "480P";
|
||||
return "480p";
|
||||
}
|
||||
|
||||
return "SD";
|
||||
|
@ -21,9 +21,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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.Text.Json" Version="4.7.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="4.7.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -37,6 +37,9 @@
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
|
@ -67,6 +67,7 @@ namespace MediaBrowser.Model.Net
|
||||
{ ".m3u8", "application/x-mpegURL" },
|
||||
{ ".map", "application/x-javascript" },
|
||||
{ ".mobi", "application/x-mobipocket-ebook" },
|
||||
{ ".opf", "application/oebps-package+xml" },
|
||||
{ ".pdf", "application/pdf" },
|
||||
{ ".rar", "application/vnd.rar" },
|
||||
{ ".srt", "application/x-subrip" },
|
||||
@ -99,6 +100,7 @@ namespace MediaBrowser.Model.Net
|
||||
{ ".ssa", "text/x-ssa" },
|
||||
{ ".css", "text/css" },
|
||||
{ ".csv", "text/csv" },
|
||||
{ ".edl", "text/plain" },
|
||||
{ ".rtf", "text/rtf" },
|
||||
{ ".txt", "text/plain" },
|
||||
{ ".vtt", "text/vtt" },
|
||||
|
@ -16,8 +16,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.4" />
|
||||
<PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
|
||||
<PackageReference Include="PlaylistsNET" Version="1.0.4" />
|
||||
<PackageReference Include="TvDbSharper" Version="3.0.1" />
|
||||
|
@ -31,8 +31,8 @@
|
||||
$('.configPage').on('pageshow', function () {
|
||||
Dashboard.showLoadingMsg();
|
||||
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
|
||||
$('#enable').checked(config.Enable);
|
||||
$('#replaceAlbumName').checked(config.ReplaceAlbumName);
|
||||
$('#enable').checked = config.Enable;
|
||||
$('#replaceAlbumName').checked = config.ReplaceAlbumName;
|
||||
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
@ -43,8 +43,8 @@
|
||||
|
||||
var form = this;
|
||||
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
|
||||
config.Enable = $('#enable', form).checked();
|
||||
config.ReplaceAlbumName = $('#replaceAlbumName', form).checked();
|
||||
config.Enable = $('#enable', form).checked;
|
||||
config.ReplaceAlbumName = $('#replaceAlbumName', form).checked;
|
||||
|
||||
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
});
|
||||
|
@ -41,8 +41,8 @@
|
||||
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
|
||||
$('#server').val(config.Server).change();
|
||||
$('#rateLimit').val(config.RateLimit).change();
|
||||
$('#enable').checked(config.Enable);
|
||||
$('#replaceArtistName').checked(config.ReplaceArtistName);
|
||||
$('#enable').checked = config.Enable;
|
||||
$('#replaceArtistName').checked = config.ReplaceArtistName;
|
||||
|
||||
Dashboard.hideLoadingMsg();
|
||||
});
|
||||
@ -55,8 +55,8 @@
|
||||
ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) {
|
||||
config.Server = $('#server', form).val();
|
||||
config.RateLimit = $('#rateLimit', form).val();
|
||||
config.Enable = $('#enable', form).checked();
|
||||
config.ReplaceArtistName = $('#replaceArtistName', form).checked();
|
||||
config.Enable = $('#enable', form).checked;
|
||||
config.ReplaceArtistName = $('#replaceArtistName', form).checked;
|
||||
|
||||
ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@ -18,8 +19,22 @@ namespace MediaBrowser.Providers.Tmdb.Movies
|
||||
{
|
||||
public class TmdbSearch
|
||||
{
|
||||
private static readonly CultureInfo EnUs = new CultureInfo("en-US");
|
||||
private const string Search3 = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}";
|
||||
private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
private static readonly Regex _cleanEnclosed = new Regex(@"\p{Ps}.*\p{Pe}", RegexOptions.Compiled);
|
||||
private static readonly Regex _cleanNonWord = new Regex(@"[\W_]+", RegexOptions.Compiled);
|
||||
private static readonly Regex _cleanStopWords = new Regex(@"\b( # Start at word boundary
|
||||
19[0-9]{2}|20[0-9]{2}| # 1900-2099
|
||||
S[0-9]{2}| # Season
|
||||
E[0-9]{2}| # Episode
|
||||
(2160|1080|720|576|480)[ip]?| # Resolution
|
||||
[xh]?264| # Encoding
|
||||
(web|dvd|bd|hdtv|hd)rip| # *Rip
|
||||
web|hdtv|mp4|bluray|ktr|dl|single|imageset|internal|doku|dubbed|retail|xxx|flac
|
||||
).* # Match rest of string",
|
||||
RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase);
|
||||
|
||||
private const string _searchURL = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}";
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IJsonSerializer _json;
|
||||
@ -61,19 +76,18 @@ namespace MediaBrowser.Providers.Tmdb.Movies
|
||||
|
||||
var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
var parsedName = _libraryManager.ParseName(name);
|
||||
var yearInName = parsedName.Year;
|
||||
name = parsedName.Name;
|
||||
year = year ?? yearInName;
|
||||
}
|
||||
// TODO: Investigate: Does this mean we are reparsing already parsed ItemLookupInfo?
|
||||
var parsedName = _libraryManager.ParseName(name);
|
||||
var yearInName = parsedName.Year;
|
||||
name = parsedName.Name;
|
||||
year ??= yearInName;
|
||||
|
||||
_logger.LogInformation("MovieDbProvider: Finding id for item: " + name);
|
||||
_logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name, year);
|
||||
var language = idInfo.MetadataLanguage.ToLowerInvariant();
|
||||
|
||||
//nope - search for it
|
||||
//var searchType = item is BoxSet ? "collection" : "movie";
|
||||
// Replace sequences of non-word characters with space
|
||||
// TMDB expects a space separated list of words make sure that is the case
|
||||
name = _cleanNonWord.Replace(name, " ").Trim();
|
||||
|
||||
var results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@ -86,36 +100,35 @@ namespace MediaBrowser.Providers.Tmdb.Movies
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: retrying alternatives should be done outside the search
|
||||
// provider so that the retry logic can be common for all search
|
||||
// providers
|
||||
if (results.Count == 0)
|
||||
{
|
||||
// try with dot and _ turned to space
|
||||
var originalName = name;
|
||||
var name2 = parsedName.Name;
|
||||
|
||||
name = name.Replace(",", " ");
|
||||
name = name.Replace(".", " ");
|
||||
name = name.Replace("_", " ");
|
||||
name = name.Replace("-", " ");
|
||||
name = name.Replace("!", " ");
|
||||
name = name.Replace("?", " ");
|
||||
// Remove things enclosed in []{}() etc
|
||||
name2 = _cleanEnclosed.Replace(name2, string.Empty);
|
||||
|
||||
var parenthIndex = name.IndexOf('(');
|
||||
if (parenthIndex != -1)
|
||||
{
|
||||
name = name.Substring(0, parenthIndex);
|
||||
}
|
||||
// Replace sequences of non-word characters with space
|
||||
name2 = _cleanNonWord.Replace(name2, " ");
|
||||
|
||||
name = name.Trim();
|
||||
// Clean based on common stop words / tokens
|
||||
name2 = _cleanStopWords.Replace(name2, string.Empty);
|
||||
|
||||
// Trim whitespace
|
||||
name2 = name2.Trim();
|
||||
|
||||
// Search again if the new name is different
|
||||
if (!string.Equals(name, originalName))
|
||||
if (!string.Equals(name2, name) && !string.IsNullOrWhiteSpace(name2))
|
||||
{
|
||||
results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name2, year);
|
||||
results = await GetSearchResults(name2, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (results.Count == 0 && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
//one more time, in english
|
||||
results = await GetSearchResults(name, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
results = await GetSearchResults(name2, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -150,7 +163,7 @@ namespace MediaBrowser.Providers.Tmdb.Movies
|
||||
throw new ArgumentException("name");
|
||||
}
|
||||
|
||||
var url3 = string.Format(Search3, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, type);
|
||||
var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, type);
|
||||
|
||||
using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
|
||||
{
|
||||
@ -179,14 +192,14 @@ namespace MediaBrowser.Providers.Tmdb.Movies
|
||||
if (!string.IsNullOrWhiteSpace(i.Release_Date))
|
||||
{
|
||||
// These dates are always in this exact format
|
||||
if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out var r))
|
||||
if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
|
||||
{
|
||||
remoteResult.PremiereDate = r.ToUniversalTime();
|
||||
remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
|
||||
}
|
||||
}
|
||||
|
||||
remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(EnUs));
|
||||
remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(_usCulture));
|
||||
|
||||
return remoteResult;
|
||||
|
||||
@ -203,7 +216,7 @@ namespace MediaBrowser.Providers.Tmdb.Movies
|
||||
throw new ArgumentException("name");
|
||||
}
|
||||
|
||||
var url3 = string.Format(Search3, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv");
|
||||
var url3 = string.Format(_searchURL, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv");
|
||||
|
||||
using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions
|
||||
{
|
||||
@ -232,14 +245,14 @@ namespace MediaBrowser.Providers.Tmdb.Movies
|
||||
if (!string.IsNullOrWhiteSpace(i.First_Air_Date))
|
||||
{
|
||||
// These dates are always in this exact format
|
||||
if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out var r))
|
||||
if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
|
||||
{
|
||||
remoteResult.PremiereDate = r.ToUniversalTime();
|
||||
remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
|
||||
}
|
||||
}
|
||||
|
||||
remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(EnUs));
|
||||
remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(_usCulture));
|
||||
|
||||
return remoteResult;
|
||||
|
||||
|
@ -66,9 +66,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api.Tests", "tests\MediaBrowser.Api.Tests\MediaBrowser.Api.Tests.csproj", "{7C93C84F-105C-48E5-A878-406FA0A5B296}"
|
||||
EndProject
|
||||
Global
|
||||
@ -185,10 +186,10 @@ Global
|
||||
{F03299F2-469F-40EF-A655-3766F97A5702}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
@ -16,7 +16,7 @@
|
||||
<PackageReference Include="AutoFixture" Version="4.11.0" />
|
||||
<PackageReference Include="AutoFixture.AutoMoq" 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="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
@ -22,4 +22,14 @@
|
||||
<ProjectReference Include="..\..\Emby.Naming\Emby.Naming.csproj" />
|
||||
</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>
|
||||
|
@ -23,9 +23,9 @@ namespace Jellyfin.Naming.Tests.Subtitles
|
||||
|
||||
var result = parser.ParseFile(input);
|
||||
|
||||
Assert.Equal(language, result.Language, true);
|
||||
Assert.Equal(isDefault, result.IsDefault);
|
||||
Assert.Equal(isForced, result.IsForced);
|
||||
Assert.Equal(language, result?.Language, true);
|
||||
Assert.Equal(isDefault, result?.IsDefault);
|
||||
Assert.Equal(isForced, result?.IsForced);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -21,7 +21,7 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
var result = new EpisodeResolver(options)
|
||||
.Resolve(path, false, null, null, true);
|
||||
|
||||
Assert.Equal(episodeNumber, result.EpisodeNumber);
|
||||
Assert.Equal(episodeNumber, result?.EpisodeNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,6 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
{
|
||||
public class DailyEpisodeTests
|
||||
{
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"/server/anything_1996.11.14.mp4", "anything", 1996, 11, 14)]
|
||||
[InlineData(@"/server/anything_1996-11-14.mp4", "anything", 1996, 11, 14)]
|
||||
@ -23,12 +21,12 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
var result = new EpisodeResolver(options)
|
||||
.Resolve(path, false);
|
||||
|
||||
Assert.Null(result.SeasonNumber);
|
||||
Assert.Null(result.EpisodeNumber);
|
||||
Assert.Equal(year, result.Year);
|
||||
Assert.Equal(month, result.Month);
|
||||
Assert.Equal(day, result.Day);
|
||||
Assert.Equal(seriesName, result.SeriesName, true);
|
||||
Assert.Null(result?.SeasonNumber);
|
||||
Assert.Null(result?.EpisodeNumber);
|
||||
Assert.Equal(year, result?.Year);
|
||||
Assert.Equal(month, result?.Month);
|
||||
Assert.Equal(day, result?.Day);
|
||||
Assert.Equal(seriesName, result?.SeriesName, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
{
|
||||
public class EpisodeNumberWithoutSeasonTests
|
||||
{
|
||||
|
||||
[Theory]
|
||||
[InlineData(8, @"The Simpsons/The Simpsons.S25E08.Steal this episode.mp4")]
|
||||
[InlineData(2, @"The Simpsons/The Simpsons - 02 - Ep Name.avi")]
|
||||
@ -30,7 +29,7 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
var result = new EpisodeResolver(options)
|
||||
.Resolve(path, false);
|
||||
|
||||
Assert.Equal(episodeNumber, result.EpisodeNumber);
|
||||
Assert.Equal(episodeNumber, result?.EpisodeNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.TV;
|
||||
using Xunit;
|
||||
|
||||
@ -35,7 +35,6 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
// TODO: [InlineData("Watchmen (2019)/Watchmen 1x03 [WEBDL-720p][EAC3 5.1][h264][-TBS] - She Was Killed by Space Junk.mkv", "Watchmen (2019)", 1, 3)]
|
||||
// TODO: [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", "The Legend of Condor Heroes 2017", 1, 7)]
|
||||
public void ParseEpisodesCorrectly(string path, string name, int season, int episode)
|
||||
|
||||
{
|
||||
NamingOptions o = new NamingOptions();
|
||||
EpisodePathParser p = new EpisodePathParser(o);
|
||||
|
@ -19,9 +19,9 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
var result = new EpisodeResolver(options)
|
||||
.Resolve(path, false);
|
||||
|
||||
Assert.Equal(seasonNumber, result.SeasonNumber);
|
||||
Assert.Equal(episodeNumber, result.EpisodeNumber);
|
||||
Assert.Equal(seriesName, result.SeriesName, true);
|
||||
Assert.Equal(seasonNumber, result?.SeasonNumber);
|
||||
Assert.Equal(episodeNumber, result?.EpisodeNumber);
|
||||
Assert.Equal(seriesName, result?.SeriesName, ignoreCase: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
var result = new EpisodeResolver(_namingOptions)
|
||||
.Resolve(path, false);
|
||||
|
||||
Assert.Equal(expected, result.SeasonNumber);
|
||||
Assert.Equal(expected, result?.SeasonNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,9 +31,9 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
var result = new EpisodeResolver(options)
|
||||
.Resolve(path, false);
|
||||
|
||||
Assert.Equal(seasonNumber, result.SeasonNumber);
|
||||
Assert.Equal(episodeNumber, result.EpisodeNumber);
|
||||
Assert.Equal(seriesName, result.SeriesName, true);
|
||||
Assert.Equal(seasonNumber, result?.SeasonNumber);
|
||||
Assert.Equal(episodeNumber, result?.EpisodeNumber);
|
||||
Assert.Equal(seriesName, result?.SeriesName, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,8 +25,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
var result =
|
||||
new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.3d.hsbs.mkv");
|
||||
|
||||
Assert.Equal("hsbs", result.Format3D);
|
||||
Assert.Equal("Oblivion", result.Name);
|
||||
Assert.Equal("hsbs", result?.Format3D);
|
||||
Assert.Equal("Oblivion", result?.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -12,7 +12,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiEdition1()
|
||||
private void TestMultiEdition1()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
@ -28,7 +28,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -37,7 +36,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiEdition2()
|
||||
private void TestMultiEdition2()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
@ -53,7 +52,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -76,7 +74,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -85,7 +82,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestLetterFolders()
|
||||
private void TestLetterFolders()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
@ -104,7 +101,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Equal(7, result.Count);
|
||||
@ -114,7 +110,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiVersionLimit()
|
||||
private void TestMultiVersionLimit()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
@ -134,7 +130,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -144,7 +139,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiVersionLimit2()
|
||||
private void TestMultiVersionLimit2()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
@ -165,7 +160,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Equal(9, result.Count);
|
||||
@ -175,7 +169,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiVersion3()
|
||||
private void TestMultiVersion3()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
@ -192,7 +186,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
@ -202,7 +195,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiVersion4()
|
||||
private void TestMultiVersion4()
|
||||
{
|
||||
// Test for false positive
|
||||
|
||||
@ -221,7 +214,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
@ -231,7 +223,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiVersion5()
|
||||
private void TestMultiVersion5()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
@ -251,7 +243,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -264,7 +255,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiVersion6()
|
||||
private void TestMultiVersion6()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
@ -284,7 +275,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -297,7 +287,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiVersion7()
|
||||
private void TestMultiVersion7()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
@ -311,7 +301,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
@ -319,7 +308,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiVersion8()
|
||||
private void TestMultiVersion8()
|
||||
{
|
||||
// This is not actually supported yet
|
||||
|
||||
@ -340,7 +329,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -353,7 +341,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiVersion9()
|
||||
private void TestMultiVersion9()
|
||||
{
|
||||
// Test for false positive
|
||||
|
||||
@ -372,7 +360,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
@ -382,7 +369,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiVersion10()
|
||||
private void TestMultiVersion10()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
@ -396,7 +383,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -406,7 +392,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestMultiVersion11()
|
||||
private void TestMultiVersion11()
|
||||
{
|
||||
// Currently not supported but we should probably handle this.
|
||||
|
||||
@ -422,7 +408,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
|
@ -368,11 +368,11 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
new FileSystemMetadata{FullName = "Bad Boys (2006) part1.mkv", IsDirectory = false},
|
||||
new FileSystemMetadata{FullName = "Bad Boys (2006) part2.mkv", IsDirectory = false},
|
||||
new FileSystemMetadata{FullName = "300 (2006) part2", IsDirectory = true},
|
||||
new FileSystemMetadata{FullName = "300 (2006) part3", IsDirectory = true},
|
||||
new FileSystemMetadata{FullName = "300 (2006) part1", IsDirectory = true}
|
||||
new FileSystemMetadata { FullName = "Bad Boys (2006) part1.mkv", IsDirectory = false },
|
||||
new FileSystemMetadata { FullName = "Bad Boys (2006) part2.mkv", IsDirectory = false },
|
||||
new FileSystemMetadata { FullName = "300 (2006) part2", IsDirectory = true },
|
||||
new FileSystemMetadata { FullName = "300 (2006) part3", IsDirectory = true },
|
||||
new FileSystemMetadata { FullName = "300 (2006) part1", IsDirectory = true }
|
||||
};
|
||||
|
||||
var resolver = GetResolver();
|
||||
|
@ -31,10 +31,10 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
var result =
|
||||
new VideoResolver(_namingOptions).ResolveFile(@"C:/Users/media/Desktop/Video Test/Movies/Oblivion/Oblivion.dvd.disc");
|
||||
|
||||
Assert.Equal("Oblivion", result.Name);
|
||||
Assert.Equal("Oblivion", result?.Name);
|
||||
}
|
||||
|
||||
private void Test(string path, bool isStub, string stubType)
|
||||
private void Test(string path, bool isStub, string? stubType)
|
||||
{
|
||||
var isStubResult = StubResolver.TryResolveFile(path, _namingOptions, out var stubTypeResult);
|
||||
|
||||
|
@ -9,9 +9,10 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
public class VideoListResolverTests
|
||||
{
|
||||
private readonly NamingOptions _namingOptions = new NamingOptions();
|
||||
|
||||
// FIXME
|
||||
// [Fact]
|
||||
public void TestStackAndExtras()
|
||||
private void TestStackAndExtras()
|
||||
{
|
||||
// No stacking here because there is no part/disc/etc
|
||||
var files = new[]
|
||||
@ -45,7 +46,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
@ -74,7 +74,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -95,7 +94,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -116,7 +114,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -138,7 +135,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -159,7 +155,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -184,7 +179,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Equal(5, result.Count);
|
||||
@ -205,7 +199,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = true,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -227,7 +220,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = true,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
@ -249,7 +241,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -271,7 +262,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -294,7 +284,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -317,7 +306,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
@ -337,7 +325,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -357,7 +344,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -378,7 +364,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -399,7 +384,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
@ -422,7 +406,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Equal(4, result.Count);
|
||||
@ -443,7 +426,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
IsDirectory = false,
|
||||
FullName = i
|
||||
|
||||
}).ToList()).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
|
@ -176,7 +176,6 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetResolveFileTestData))]
|
||||
public void ResolveFile_ValidFileName_Success(VideoFileInfo expectedResult)
|
||||
@ -184,17 +183,17 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
var result = new VideoResolver(_namingOptions).ResolveFile(expectedResult.Path);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(result.Path, expectedResult.Path);
|
||||
Assert.Equal(result.Container, expectedResult.Container);
|
||||
Assert.Equal(result.Name, expectedResult.Name);
|
||||
Assert.Equal(result.Year, expectedResult.Year);
|
||||
Assert.Equal(result.ExtraType, expectedResult.ExtraType);
|
||||
Assert.Equal(result.Format3D, expectedResult.Format3D);
|
||||
Assert.Equal(result.Is3D, expectedResult.Is3D);
|
||||
Assert.Equal(result.IsStub, expectedResult.IsStub);
|
||||
Assert.Equal(result.StubType, expectedResult.StubType);
|
||||
Assert.Equal(result.IsDirectory, expectedResult.IsDirectory);
|
||||
Assert.Equal(result.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension);
|
||||
Assert.Equal(result?.Path, expectedResult.Path);
|
||||
Assert.Equal(result?.Container, expectedResult.Container);
|
||||
Assert.Equal(result?.Name, expectedResult.Name);
|
||||
Assert.Equal(result?.Year, expectedResult.Year);
|
||||
Assert.Equal(result?.ExtraType, expectedResult.ExtraType);
|
||||
Assert.Equal(result?.Format3D, expectedResult.Format3D);
|
||||
Assert.Equal(result?.Is3D, expectedResult.Is3D);
|
||||
Assert.Equal(result?.IsStub, expectedResult.IsStub);
|
||||
Assert.Equal(result?.StubType, expectedResult.StubType);
|
||||
Assert.Equal(result?.IsDirectory, expectedResult.IsDirectory);
|
||||
Assert.Equal(result?.FileNameWithoutExtension, expectedResult.FileNameWithoutExtension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
|
||||
|
Loading…
Reference in New Issue
Block a user