commit
757643e326
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
@ -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,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'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
Loading…
Reference in new issue