commit
e4838b0faa
@ -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,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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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": "อัลบั้ม"
|
||||
}
|
@ -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>
|
||||
|
@ -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'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,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; }
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue