Исправление: том ./mods:/lampac/mods в compose и копии 10 модулей с enable:true; конфиг без SkipModules
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Telemetry.Models;
|
||||
|
||||
namespace Telemetry;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
public DbSet<LogModelSql> Logs { get; set; } = null!;
|
||||
public DbSet<UserInfoModelSql> Users { get; set; } = null!;
|
||||
|
||||
public AppDbContext() : base() { }
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
if (!optionsBuilder.IsConfigured)
|
||||
{
|
||||
var dbPath = ModInit.DbPath;
|
||||
var dbDir = Path.GetDirectoryName(dbPath);
|
||||
if (dbDir != null && !Directory.Exists(dbDir)) Directory.CreateDirectory(dbDir);
|
||||
optionsBuilder.UseSqlite($"Data Source={dbPath};Cache=Shared");
|
||||
optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<LogModelSql>()
|
||||
.HasIndex(l => l.Time);
|
||||
|
||||
modelBuilder.Entity<LogModelSql>()
|
||||
.HasIndex(l => new { l.Uid, l.Time });
|
||||
|
||||
modelBuilder.Entity<LogModelSql>()
|
||||
.HasIndex(l => new { l.Balancer, l.Time });
|
||||
|
||||
modelBuilder.Entity<LogModelSql>()
|
||||
.HasIndex(l => l.UnfoId);
|
||||
|
||||
modelBuilder.Entity<UserInfoModelSql>()
|
||||
.HasKey(u => u.Id);
|
||||
|
||||
modelBuilder.Entity<UserInfoModelSql>()
|
||||
.HasIndex(u => u.Ip);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shared;
|
||||
using Shared.Attributes;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Telemetry.Models;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Telemetry.Controllers;
|
||||
|
||||
[Authorization(redirectUri: "/adminpanel/auth")]
|
||||
[Route("/telemetry")]
|
||||
public class TelemetryController : BaseController
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _contextFactory;
|
||||
|
||||
public TelemetryController(IDbContextFactory<AppDbContext> contextFactory)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
}
|
||||
|
||||
private class MovieRawDto
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public string? Tmdb { get; set; }
|
||||
public string? Kp { get; set; }
|
||||
public string? Imdb { get; set; }
|
||||
public bool IsTv { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
private static (DateTime start, DateTime end) GetPeriod(int days)
|
||||
{
|
||||
var now = DateTime.UtcNow.Date;
|
||||
var periodStart = days == 0 ? now : (days == -1 ? now.AddDays(-1) : now.AddDays(-days));
|
||||
var periodEnd = days == -1 ? now : DateTime.UtcNow;
|
||||
return (periodStart, periodEnd);
|
||||
}
|
||||
|
||||
private static List<object> MapMovies(List<MovieRawDto> raw)
|
||||
{
|
||||
return raw.Select(m => new
|
||||
{
|
||||
title = m.Title,
|
||||
id = m.Tmdb ?? m.Kp ?? m.Imdb ?? m.Title,
|
||||
tmdb = m.Tmdb,
|
||||
kp = m.Kp,
|
||||
imdb = m.Imdb,
|
||||
count = m.Count,
|
||||
source = m.Tmdb != null ? "tmdb" : m.Kp != null ? "kinopoisk" : m.Imdb != null ? "imdb" : "unknown",
|
||||
isTv = m.IsTv
|
||||
}).Cast<object>().ToList();
|
||||
}
|
||||
|
||||
private IQueryable<LogModelSql> ApplySearchFilter(AppDbContext db, IQueryable<LogModelSql> query, string? search)
|
||||
{
|
||||
if (string.IsNullOrEmpty(search)) return query;
|
||||
|
||||
var rawSearch = search;
|
||||
var encodedSearch = Uri.EscapeDataString(search);
|
||||
var encodedSearchPlus = encodedSearch.Replace("%20", "+");
|
||||
|
||||
return query.Where(l =>
|
||||
l.Uid.Contains(rawSearch) ||
|
||||
l.Uri.Contains(rawSearch) ||
|
||||
l.Uri.Contains(encodedSearch) ||
|
||||
l.Uri.Contains(encodedSearchPlus) ||
|
||||
(l.Balancer != null && l.Balancer.Contains(rawSearch)) ||
|
||||
db.Users.Any(u => u.Id == l.UnfoId && (u.Ip.Contains(rawSearch) || u.UserAgent.Contains(rawSearch)))
|
||||
);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
public ActionResult Index()
|
||||
{
|
||||
var path = Path.Combine(ModInit.modpath, "index.html");
|
||||
if (!System.IO.File.Exists(path)) return Content("UI missing", "text/plain");
|
||||
var html = System.IO.File.ReadAllText(path, Encoding.UTF8);
|
||||
return Content(html, "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
[HttpGet("api/stats")]
|
||||
public async Task<IActionResult> Stats(string? search, int days = 30)
|
||||
{
|
||||
await using var db = await _contextFactory.CreateDbContextAsync();
|
||||
var query = db.Logs.AsNoTracking();
|
||||
query = ApplySearchFilter(db, query, search);
|
||||
|
||||
var (periodStart, periodEnd) = GetPeriod(days);
|
||||
var now = DateTime.UtcNow.Date;
|
||||
|
||||
int total;
|
||||
int today;
|
||||
if (days == 0)
|
||||
{
|
||||
today = await query.Where(l => l.Time >= now.Date && l.Time < periodEnd).CountAsync();
|
||||
total = today;
|
||||
}
|
||||
else if (days == -1)
|
||||
{
|
||||
total = await query.Where(l => l.Time >= periodStart && l.Time < periodEnd).CountAsync();
|
||||
today = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
total = await query.Where(l => l.Time >= periodStart && l.Time < periodEnd).CountAsync();
|
||||
today = await query.Where(l => l.Time >= now.Date && l.Time < periodEnd).CountAsync();
|
||||
}
|
||||
|
||||
var uids = await query.Where(l => l.Time >= periodStart && l.Time < periodEnd && l.Uid != "anonymous" && !string.IsNullOrEmpty(l.Uid)).Select(l => l.Uid).Distinct().CountAsync();
|
||||
|
||||
var validLogs = query.Where(l => l.Time >= periodStart && l.Time < periodEnd && l.UnfoId != null);
|
||||
var ips = await validLogs.Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.Ip).Where(ip => ip != null && ip != "").Distinct().CountAsync();
|
||||
var uas = await validLogs.Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.UserAgent).Where(ua => ua != null && ua != "").Distinct().CountAsync();
|
||||
|
||||
return new JsonResult(new { success = true, data = new { today, total, uids, ips, uas } });
|
||||
}
|
||||
|
||||
[HttpGet("api/logs")]
|
||||
public async Task<IActionResult> Logs(string? search, int days = 30, int skip = 0, int take = 100)
|
||||
{
|
||||
take = Math.Min(take, 1000);
|
||||
await using var db = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
var query = db.Logs.AsNoTracking();
|
||||
var (periodStart, periodEnd) = GetPeriod(days);
|
||||
|
||||
query = query.Where(l => l.Time >= periodStart && l.Time < periodEnd);
|
||||
query = ApplySearchFilter(db, query, search);
|
||||
|
||||
var logs = await query.OrderByDescending(l => l.Time).Skip(skip).Take(take)
|
||||
.GroupJoin(db.Users, l => l.UnfoId, u => u.Id, (l, users) => new { Log = l, Users = users })
|
||||
.SelectMany(x => x.Users.DefaultIfEmpty(), (x, u) => new
|
||||
{
|
||||
id = x.Log.Id,
|
||||
time = x.Log.Time,
|
||||
uri = x.Log.Uri,
|
||||
uid = x.Log.Uid,
|
||||
ip = u != null ? u.Ip : "unknown",
|
||||
userAgent = u != null ? u.UserAgent : "unknown",
|
||||
balancer = x.Log.Balancer,
|
||||
statusCode = x.Log.StatusCode,
|
||||
duration = x.Log.DurationMs
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return new JsonResult(new { success = true, data = logs });
|
||||
}
|
||||
|
||||
[HttpGet("api/chart")]
|
||||
public async Task<IActionResult> Chart(string? search, int days = 30)
|
||||
{
|
||||
var (periodStart, periodEnd) = GetPeriod(days);
|
||||
|
||||
await using var db = await _contextFactory.CreateDbContextAsync();
|
||||
var query = db.Logs.AsNoTracking();
|
||||
query = ApplySearchFilter(db, query, search);
|
||||
|
||||
var data = await query
|
||||
.Where(l => l.Time >= periodStart && l.Time < periodEnd)
|
||||
.GroupBy(l => l.Time.Date)
|
||||
.Select(g => new { date = g.Key, count = g.Count() })
|
||||
.OrderBy(x => x.date)
|
||||
.ToListAsync();
|
||||
|
||||
return new JsonResult(new { success = true, data = data });
|
||||
}
|
||||
|
||||
[HttpGet("api/tops")]
|
||||
public async Task<IActionResult> GetTops(string? search, [FromQuery] int days = 7)
|
||||
{
|
||||
await using var db = await _contextFactory.CreateDbContextAsync();
|
||||
var (periodStart, periodEnd) = GetPeriod(days);
|
||||
|
||||
var query = db.Logs.AsNoTracking();
|
||||
query = ApplySearchFilter(db, query, search);
|
||||
var baseQuery = query.Where(l => l.Time >= periodStart && l.Time < periodEnd);
|
||||
|
||||
var topBalancers = await baseQuery
|
||||
.Where(l => l.Balancer != null && l.Balancer != "" && !l.Balancer.StartsWith("18+ "))
|
||||
.GroupBy(l => l.Balancer)
|
||||
.Select(g => new { balancer = g.Key, count = g.Count() })
|
||||
.OrderByDescending(x => x.count)
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
|
||||
var topAdult = await baseQuery
|
||||
.Where(l => l.Balancer != null && l.Balancer.StartsWith("18+ "))
|
||||
.GroupBy(l => l.Balancer)
|
||||
.Select(g => new { balancer = g.Key!.Substring(4), count = g.Count() })
|
||||
.OrderByDescending(x => x.count)
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
|
||||
var topUsers = await baseQuery
|
||||
.Where(l => l.Uid != "anonymous" && l.Uid != null && l.Uid != "")
|
||||
.GroupBy(l => l.Uid)
|
||||
.Select(g => new { uid = g.Key, count = g.Count() })
|
||||
.OrderByDescending(x => x.count)
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
|
||||
var topMoviesRaw = await baseQuery
|
||||
.Where(l => l.MovieTitle != null && l.MovieTitle != "")
|
||||
.GroupBy(l => l.MovieTitle)
|
||||
.Select(g => new MovieRawDto
|
||||
{
|
||||
Title = g.Key,
|
||||
Tmdb = g.Max(l => l.TmdbId),
|
||||
Kp = g.Max(l => l.KpId),
|
||||
Imdb = g.Max(l => l.ImdbId),
|
||||
IsTv = g.Max(l => l.IsTv ? 1 : 0) == 1,
|
||||
Count = g.Count()
|
||||
})
|
||||
.OrderByDescending(x => x.Count)
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
|
||||
var topMovies = MapMovies(topMoviesRaw);
|
||||
|
||||
var userAgents = await baseQuery
|
||||
.Where(l => l.UnfoId != null)
|
||||
.Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.UserAgent)
|
||||
.GroupBy(ua => ua)
|
||||
.Select(g => new { ua = g.Key, count = g.Count() })
|
||||
.OrderByDescending(x => x.count)
|
||||
.Take(1000)
|
||||
.ToListAsync();
|
||||
|
||||
var devices = userAgents
|
||||
.Select(x => new { name = ParseDevice(x.ua), count = x.count })
|
||||
.GroupBy(x => x.name)
|
||||
.Select(g => new { name = g.Key, count = g.Sum(x => x.count) })
|
||||
.OrderByDescending(x => x.count)
|
||||
.ToList();
|
||||
|
||||
var hours = await baseQuery
|
||||
.GroupBy(l => l.Time.Hour)
|
||||
.Select(g => new { hour = g.Key, count = g.Count() })
|
||||
.OrderBy(x => x.hour)
|
||||
.ToListAsync();
|
||||
|
||||
var seriesCount = await baseQuery.Where(l => l.IsTv).CountAsync();
|
||||
var moviesCount = await baseQuery.Where(l => l.MovieTitle != null && l.MovieTitle != "" && !l.IsTv).CountAsync();
|
||||
var animeCount = await baseQuery.Where(l => l.Uri.Contains("anime=1") || l.Uri.Contains("anime=true")).CountAsync();
|
||||
|
||||
var contentTypes = new[] {
|
||||
new { name = "Фильмы", count = moviesCount },
|
||||
new { name = "Сериалы", count = seriesCount },
|
||||
new { name = "Аниме", count = animeCount }
|
||||
}.Where(x => x.count > 0).ToList();
|
||||
|
||||
return new JsonResult(new { success = true, data = new { balancers = topBalancers, adult = topAdult, users = topUsers, movies = topMovies, devices, hours, contentTypes } });
|
||||
}
|
||||
|
||||
private string ParseDevice(string? ua)
|
||||
{
|
||||
if (ua == null) return "Другие";
|
||||
ua = ua.ToLower();
|
||||
if (ua.Contains("tizen") || ua.Contains("samsung")) return "Tizen";
|
||||
if (ua.Contains("web0s") || ua.Contains("webos")) return "WebOS";
|
||||
if (ua.Contains("android")) return "Android";
|
||||
if (ua.Contains("iphone") || ua.Contains("ipad") || ua.Contains("mac os")) return "Apple";
|
||||
if (ua.Contains("windows")) return "Windows";
|
||||
if (ua.Contains("linux")) return "Linux";
|
||||
return "Другие";
|
||||
}
|
||||
|
||||
[HttpGet("api/user")]
|
||||
public async Task<IActionResult> UserInfo(string uid, int days = 30)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uid)) return BadRequest();
|
||||
|
||||
await using var db = await _contextFactory.CreateDbContextAsync();
|
||||
var (periodStart, periodEnd) = GetPeriod(days);
|
||||
|
||||
var baseQuery = db.Logs.AsNoTracking().Where(l => l.Uid == uid && l.Time >= periodStart && l.Time < periodEnd);
|
||||
var count = await baseQuery.CountAsync();
|
||||
|
||||
if (count == 0) return new JsonResult(new { success = false });
|
||||
|
||||
var totalRequests = count;
|
||||
var firstSeen = await baseQuery.MinAsync(l => l.Time);
|
||||
var lastSeen = await baseQuery.MaxAsync(l => l.Time);
|
||||
|
||||
var ips = await baseQuery.Where(l => l.UnfoId != null).Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.Ip).Where(ip => ip != null && ip != "").Distinct().ToListAsync();
|
||||
var userAgents = await baseQuery.Where(l => l.UnfoId != null).Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.UserAgent).Where(ua => ua != null && ua != "").Distinct().ToListAsync();
|
||||
|
||||
var balancers = await baseQuery.Where(l => l.Balancer != null && l.Balancer != "")
|
||||
.GroupBy(l => l.Balancer)
|
||||
.Select(g => new { name = g.Key, count = g.Count() })
|
||||
.OrderByDescending(x => x.count)
|
||||
.ToListAsync();
|
||||
|
||||
var isHourly = days <= 1;
|
||||
var timeline = await (isHourly
|
||||
? baseQuery.GroupBy(l => new DateTime(l.Time.Year, l.Time.Month, l.Time.Day, l.Time.Hour, 0, 0)).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync()
|
||||
: baseQuery.GroupBy(l => l.Time.Date).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync());
|
||||
|
||||
var moviesRaw = await baseQuery
|
||||
.Where(l => l.MovieTitle != null && l.MovieTitle != "")
|
||||
.GroupBy(l => l.MovieTitle)
|
||||
.Select(g => new MovieRawDto
|
||||
{
|
||||
Title = g.Key,
|
||||
Tmdb = g.Max(l => l.TmdbId),
|
||||
Kp = g.Max(l => l.KpId),
|
||||
Imdb = g.Max(l => l.ImdbId),
|
||||
IsTv = g.Max(l => l.IsTv ? 1 : 0) == 1,
|
||||
Count = g.Count()
|
||||
})
|
||||
.OrderByDescending(x => x.Count)
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
|
||||
var movies = MapMovies(moviesRaw);
|
||||
|
||||
return new JsonResult(new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
totalRequests,
|
||||
firstSeen,
|
||||
lastSeen,
|
||||
ips,
|
||||
userAgents,
|
||||
balancers,
|
||||
timeline,
|
||||
movies
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("api/ip")]
|
||||
public async Task<IActionResult> IpInfo(string ip, int days = 30)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ip)) return BadRequest();
|
||||
|
||||
await using var db = await _contextFactory.CreateDbContextAsync();
|
||||
var (periodStart, periodEnd) = GetPeriod(days);
|
||||
|
||||
var baseQuery = db.Logs.AsNoTracking().Where(l => l.Time >= periodStart && l.Time < periodEnd && l.UnfoId != null && db.Users.Any(u => u.Id == l.UnfoId && u.Ip == ip));
|
||||
var count = await baseQuery.CountAsync();
|
||||
|
||||
if (count == 0) return new JsonResult(new { success = false });
|
||||
|
||||
var totalRequests = count;
|
||||
var firstSeen = await baseQuery.MinAsync(l => l.Time);
|
||||
var lastSeen = await baseQuery.MaxAsync(l => l.Time);
|
||||
|
||||
var uids = await baseQuery.Where(l => l.Uid != "anonymous" && l.Uid != null && l.Uid != "").Select(l => l.Uid).Distinct().ToListAsync();
|
||||
var userAgents = await baseQuery.Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.UserAgent).Where(ua => ua != null && ua != "").Distinct().ToListAsync();
|
||||
|
||||
var balancers = await baseQuery.Where(l => l.Balancer != null && l.Balancer != "")
|
||||
.GroupBy(l => l.Balancer)
|
||||
.Select(g => new { name = g.Key, count = g.Count() })
|
||||
.OrderByDescending(x => x.count)
|
||||
.ToListAsync();
|
||||
|
||||
var isHourly = days <= 1;
|
||||
var timeline = await (isHourly
|
||||
? baseQuery.GroupBy(l => new DateTime(l.Time.Year, l.Time.Month, l.Time.Day, l.Time.Hour, 0, 0)).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync()
|
||||
: baseQuery.GroupBy(l => l.Time.Date).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync());
|
||||
|
||||
var moviesRaw = await baseQuery
|
||||
.Where(l => l.MovieTitle != null && l.MovieTitle != "")
|
||||
.GroupBy(l => l.MovieTitle)
|
||||
.Select(g => new MovieRawDto
|
||||
{
|
||||
Title = g.Key,
|
||||
Tmdb = g.Max(l => l.TmdbId),
|
||||
Kp = g.Max(l => l.KpId),
|
||||
Imdb = g.Max(l => l.ImdbId),
|
||||
IsTv = g.Max(l => l.IsTv ? 1 : 0) == 1,
|
||||
Count = g.Count()
|
||||
})
|
||||
.OrderByDescending(x => x.Count)
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
|
||||
var movies = MapMovies(moviesRaw);
|
||||
|
||||
return new JsonResult(new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
totalRequests,
|
||||
firstSeen,
|
||||
lastSeen,
|
||||
uids,
|
||||
userAgents,
|
||||
balancers,
|
||||
timeline,
|
||||
movies
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("api/settings")]
|
||||
public IActionResult GetSettings()
|
||||
{
|
||||
return new JsonResult(new
|
||||
{
|
||||
success = true,
|
||||
data = new
|
||||
{
|
||||
prefixes = string.Join("\n", RequestListener.SkipPrefixes),
|
||||
extensions = string.Join("\n", RequestListener.SkipExtensions),
|
||||
regexes = string.Join("\n", RequestListener.RegexFilters),
|
||||
ips = string.Join("\n", RequestListener.BlacklistIps),
|
||||
rateLimit = RequestListener.RateLimitMs
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("api/settings")]
|
||||
public IActionResult UpdateSettings([FromForm] string? prefixes, [FromForm] string? extensions, [FromForm] string? regexes, [FromForm] string? ips, [FromForm] int? rateLimit)
|
||||
{
|
||||
if (prefixes != null) RequestListener.SkipPrefixes = prefixes.Split(new[] { '\n', ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
|
||||
if (extensions != null) RequestListener.SkipExtensions = extensions.Split(new[] { '\n', ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
|
||||
if (regexes != null)
|
||||
{
|
||||
var patterns = regexes.Split(new[] { '\n', ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
|
||||
var invalid = new List<string>();
|
||||
foreach (var p in patterns)
|
||||
{
|
||||
try { System.Text.RegularExpressions.Regex.Match("", p, System.Text.RegularExpressions.RegexOptions.None, TimeSpan.FromMilliseconds(50)); }
|
||||
catch { invalid.Add(p); }
|
||||
}
|
||||
if (invalid.Count > 0)
|
||||
return new JsonResult(new { success = false, error = $"Невалидные regex: {string.Join(", ", invalid)}" });
|
||||
RequestListener.RegexFilters = patterns;
|
||||
}
|
||||
if (ips != null) RequestListener.BlacklistIps = ips.Split(new[] { '\n', ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
|
||||
if (rateLimit.HasValue) RequestListener.RateLimitMs = rateLimit.Value;
|
||||
|
||||
RequestListener.SaveSettings();
|
||||
|
||||
return new JsonResult(new { success = true });
|
||||
}
|
||||
|
||||
[HttpGet("api/balancer")]
|
||||
public async Task<IActionResult> BalancerInfo(string balancer, int days = 30)
|
||||
{
|
||||
if (string.IsNullOrEmpty(balancer)) return BadRequest();
|
||||
|
||||
await using var db = await _contextFactory.CreateDbContextAsync();
|
||||
var (periodStart, periodEnd) = GetPeriod(days);
|
||||
var now = DateTime.UtcNow.Date;
|
||||
|
||||
var baseQuery = db.Logs.AsNoTracking().Where(l => l.Balancer == balancer && l.Time >= periodStart && l.Time < periodEnd);
|
||||
|
||||
var total = await baseQuery.CountAsync();
|
||||
var today = await baseQuery.Where(l => l.Time >= now.Date).CountAsync();
|
||||
|
||||
var topUids = await baseQuery.Where(l => l.Uid != "anonymous" && l.Uid != null && l.Uid != "")
|
||||
.GroupBy(l => l.Uid)
|
||||
.Select(g => new { uid = g.Key, count = g.Count() })
|
||||
.OrderByDescending(x => x.count).Take(10).ToListAsync();
|
||||
|
||||
var topIps = await baseQuery.Where(l => l.UnfoId != null)
|
||||
.Join(db.Users, l => l.UnfoId, u => u.Id, (l, u) => u.Ip)
|
||||
.Where(ip => ip != null && ip != "")
|
||||
.GroupBy(ip => ip)
|
||||
.Select(g => new { ip = g.Key, count = g.Count() })
|
||||
.OrderByDescending(x => x.count).Take(10).ToListAsync();
|
||||
|
||||
var isHourly = days <= 1;
|
||||
var timeline = await (isHourly
|
||||
? baseQuery.GroupBy(l => new DateTime(l.Time.Year, l.Time.Month, l.Time.Day, l.Time.Hour, 0, 0)).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync()
|
||||
: baseQuery.GroupBy(l => l.Time.Date).Select(g => new { date = g.Key, count = g.Count() }).OrderBy(x => x.date).ToListAsync());
|
||||
|
||||
var moviesRaw = await baseQuery
|
||||
.Where(l => l.MovieTitle != null && l.MovieTitle != "")
|
||||
.GroupBy(l => l.MovieTitle)
|
||||
.Select(g => new MovieRawDto
|
||||
{
|
||||
Title = g.Key,
|
||||
Tmdb = g.Max(l => l.TmdbId),
|
||||
Kp = g.Max(l => l.KpId),
|
||||
Imdb = g.Max(l => l.ImdbId),
|
||||
IsTv = g.Max(l => l.IsTv ? 1 : 0) == 1,
|
||||
Count = g.Count()
|
||||
})
|
||||
.OrderByDescending(x => x.Count)
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
|
||||
var movies = MapMovies(moviesRaw);
|
||||
|
||||
return new JsonResult(new { success = true, data = new { total, today, uids = topUids, ips = topIps, timeline, movies } });
|
||||
}
|
||||
|
||||
[HttpPost("api/logs/delete")]
|
||||
public async Task<IActionResult> DeleteLogs([FromForm] string? search, [FromForm] int days = 30)
|
||||
{
|
||||
await using var db = await _contextFactory.CreateDbContextAsync();
|
||||
var now = DateTime.UtcNow.Date;
|
||||
var periodStart = days == 0 ? now : (days == -1 ? now.AddDays(-1) : now.AddDays(-days));
|
||||
var periodEnd = days == -1 ? now : DateTime.UtcNow;
|
||||
|
||||
var query = db.Logs.Where(l => l.Time >= periodStart && l.Time < periodEnd);
|
||||
query = ApplySearchFilter(db, query, search);
|
||||
|
||||
var deleted = await query.ExecuteDeleteAsync();
|
||||
return new JsonResult(new { success = true, deleted });
|
||||
}
|
||||
|
||||
[HttpGet("api/export")]
|
||||
public async Task<IActionResult> ExportData()
|
||||
{
|
||||
await using var db = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
var result = await db.Logs.AsNoTracking()
|
||||
.OrderByDescending(l => l.Id)
|
||||
.Take(50000)
|
||||
.GroupJoin(db.Users, l => l.UnfoId, u => u.Id, (l, users) => new { Log = l, Users = users })
|
||||
.SelectMany(x => x.Users.DefaultIfEmpty(), (x, u) => new
|
||||
{
|
||||
time = x.Log.Time,
|
||||
uri = x.Log.Uri,
|
||||
uid = x.Log.Uid,
|
||||
ip = u != null ? u.Ip : "",
|
||||
userAgent = u != null ? u.UserAgent : "",
|
||||
balancer = x.Log.Balancer,
|
||||
statusCode = x.Log.StatusCode
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return new JsonResult(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Shared;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Base;
|
||||
using Shared.Models.Events;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using Telemetry.Models;
|
||||
|
||||
namespace Telemetry;
|
||||
|
||||
public class ModInit : IModuleLoaded, IModuleConfigure
|
||||
{
|
||||
public static string modpath { get; private set; } = string.Empty;
|
||||
public static readonly string DbPath = Path.Combine(AppContext.BaseDirectory, "database", "Telemetry", "stats.db");
|
||||
private static Timer? _updateDbTimer;
|
||||
private static Timer? _cleanupTimer;
|
||||
private static int _updatingDb = 0;
|
||||
|
||||
public void Configure(ConfigureModel app)
|
||||
{
|
||||
app.services.AddDbContextFactory<AppDbContext>(options =>
|
||||
{
|
||||
options.UseSqlite($"Data Source={DbPath};Cache=Shared");
|
||||
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
|
||||
});
|
||||
}
|
||||
|
||||
public void Loaded(InitspaceModel initspace)
|
||||
{
|
||||
modpath = initspace.path;
|
||||
|
||||
var dbDir = Path.GetDirectoryName(DbPath);
|
||||
if (dbDir != null) Directory.CreateDirectory(dbDir);
|
||||
|
||||
using (var sqlDb = new AppDbContext())
|
||||
{
|
||||
sqlDb.Database.EnsureCreated();
|
||||
try
|
||||
{
|
||||
sqlDb.Database.ExecuteSqlRaw("PRAGMA journal_mode = WAL;");
|
||||
sqlDb.Database.ExecuteSqlRaw("PRAGMA synchronous = NORMAL;");
|
||||
sqlDb.Database.ExecuteSqlRaw("PRAGMA cache_size = -64000;");
|
||||
sqlDb.Database.ExecuteSqlRaw("PRAGMA temp_store = MEMORY;");
|
||||
sqlDb.Database.ExecuteSqlRaw("PRAGMA mmap_size = 33554432;");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
_updateDbTimer?.Dispose();
|
||||
_cleanupTimer?.Dispose();
|
||||
|
||||
_updateDbTimer = new Timer(UpdateDb, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
|
||||
_cleanupTimer = new Timer(CleanupDb, null, TimeSpan.FromMinutes(1), TimeSpan.FromHours(12));
|
||||
|
||||
RequestListener.LoadSettings();
|
||||
|
||||
EventListener.MiddlewareAsync -= RequestListener.InvokeAsync;
|
||||
EventListener.MiddlewareAsync += RequestListener.InvokeAsync;
|
||||
|
||||
Console.WriteLine("[Telemetry] Module loaded.");
|
||||
}
|
||||
|
||||
private static void UpdateDb(object? state)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _updatingDb, 1) == 1) return;
|
||||
try
|
||||
{
|
||||
using var sqlDb = new AppDbContext();
|
||||
sqlDb.ChangeTracker.AutoDetectChangesEnabled = false;
|
||||
|
||||
var batch = new List<(LogModelSql log, UserInfoModelSql user)>();
|
||||
while (batch.Count < 2000 && RequestListener.Queue.TryDequeue(out var item))
|
||||
{
|
||||
RequestListener.DequeueItem();
|
||||
batch.Add(item);
|
||||
}
|
||||
|
||||
if (batch.Count == 0) return;
|
||||
|
||||
var userIds = batch.Select(b => b.user.Id).Distinct().ToList();
|
||||
var existingUsers = sqlDb.Users.Where(u => userIds.Contains(u.Id)).Select(u => u.Id).ToHashSet();
|
||||
|
||||
foreach (var item in batch.OrderBy(i => i.log.Time))
|
||||
{
|
||||
if (!existingUsers.Contains(item.user.Id))
|
||||
{
|
||||
sqlDb.Users.Add(item.user);
|
||||
existingUsers.Add(item.user.Id);
|
||||
}
|
||||
sqlDb.Logs.Add(item.log);
|
||||
}
|
||||
sqlDb.SaveChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Telemetry] DB Update Error: {ex.Message}");
|
||||
}
|
||||
finally { Interlocked.Exchange(ref _updatingDb, 0); }
|
||||
}
|
||||
|
||||
private static void CleanupDb(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Settings should be configurable, hardcoded for now: 90 days or max 500MB DB
|
||||
int logDay = 90;
|
||||
var cutoff = DateTime.UtcNow.AddDays(-logDay);
|
||||
|
||||
using var sqlDb = new AppDbContext();
|
||||
int batchSize = 5000;
|
||||
|
||||
// Delete old records
|
||||
while (true)
|
||||
{
|
||||
var ids = sqlDb.Logs.Where(l => l.Time < cutoff).Select(l => l.Id).Take(batchSize).ToList();
|
||||
if (ids.Count == 0) break;
|
||||
sqlDb.Logs.Where(l => ids.Contains(l.Id)).ExecuteDelete();
|
||||
if (ids.Count < batchSize) break;
|
||||
}
|
||||
|
||||
// Delete orphaned users who have no logs left
|
||||
sqlDb.Users.Where(u => !sqlDb.Logs.Any(l => l.UnfoId == u.Id)).ExecuteDelete();
|
||||
|
||||
// Run VACUUM to reclaim deleted disk space
|
||||
try { sqlDb.Database.ExecuteSqlRaw("VACUUM;"); } catch { }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Telemetry] DB Cleanup Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
EventListener.MiddlewareAsync -= RequestListener.InvokeAsync;
|
||||
_updateDbTimer?.Dispose();
|
||||
_cleanupTimer?.Dispose();
|
||||
Console.WriteLine("[Telemetry] Module disposed.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Telemetry.Models;
|
||||
|
||||
public class LogModelSql
|
||||
{
|
||||
[Key]
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime Time { get; set; }
|
||||
|
||||
[MaxLength(2048)]
|
||||
public string Uri { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(256)]
|
||||
public string Uid { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(64)]
|
||||
public string? UnfoId { get; set; }
|
||||
|
||||
public int DurationMs { get; set; }
|
||||
|
||||
[MaxLength(128)]
|
||||
public string? Balancer { get; set; }
|
||||
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
[MaxLength(256)]
|
||||
public string? MovieTitle { get; set; }
|
||||
|
||||
[MaxLength(32)]
|
||||
public string? TmdbId { get; set; }
|
||||
|
||||
[MaxLength(32)]
|
||||
public string? KpId { get; set; }
|
||||
|
||||
[MaxLength(32)]
|
||||
public string? ImdbId { get; set; }
|
||||
|
||||
public bool IsTv { get; set; }
|
||||
}
|
||||
|
||||
public class UserInfoModelSql
|
||||
{
|
||||
[Key]
|
||||
[MaxLength(64)]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(64)]
|
||||
public string Ip { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string UserAgent { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Shared;
|
||||
using Shared.Models;
|
||||
using Shared.Models.Base;
|
||||
using Shared.Models.Events;
|
||||
using Shared.Models.Module;
|
||||
using Shared.Models.Module.Interfaces;
|
||||
using Telemetry.Models;
|
||||
|
||||
namespace Telemetry;
|
||||
|
||||
public static class RequestListener
|
||||
{
|
||||
public static readonly ConcurrentQueue<(LogModelSql log, UserInfoModelSql user)> Queue = new();
|
||||
private static int _queueSize = 0;
|
||||
private const int MaxQueueSize = 50000;
|
||||
|
||||
private static readonly MemoryCache _rateLimitCache = new(new MemoryCacheOptions { SizeLimit = 100_000 });
|
||||
private static readonly MemoryCache _balancerDedupCache = new(new MemoryCacheOptions { SizeLimit = 50_000 });
|
||||
private static readonly MemoryCache _userIdCache = new(new MemoryCacheOptions { SizeLimit = 50_000 });
|
||||
|
||||
private static readonly string _settingsPath = Path.Combine(AppContext.BaseDirectory, "database", "Telemetry", "settings.json");
|
||||
|
||||
public static volatile string[] SkipPrefixes = {
|
||||
"/.well-known", "/admin/health", "/admin/ping",
|
||||
"/lite/telemetry", "/telemetry", "/adminpanel", "/stats", "/weblog",
|
||||
"/storage/", "/timecode/", "/bookmark/", "/tmdb/api/", "/lite/events", "/externalids", "/sisi"
|
||||
};
|
||||
public static volatile string[] SkipExtensions = { ".js", ".css", ".svg", ".png", ".jpg", ".woff", ".ico" };
|
||||
public static volatile string[] RegexFilters = { };
|
||||
private static volatile HashSet<string> _blacklistIps = new();
|
||||
public static volatile int RateLimitMs = 33;
|
||||
|
||||
public static string[] BlacklistIps
|
||||
{
|
||||
get => _blacklistIps.ToArray();
|
||||
set => _blacklistIps = new HashSet<string>(value, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsBlacklistedIp(string ip) => _blacklistIps.Contains(ip);
|
||||
|
||||
public static void DequeueItem() => Interlocked.Decrement(ref _queueSize);
|
||||
|
||||
public static Task<bool> InvokeAsync(bool first, EventMiddleware e)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
e.httpContext.Items["Telemetry_StartTime"] = DateTime.UtcNow;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
var httpContext = e.httpContext;
|
||||
var requestInfo = httpContext.Features.Get<RequestModel>();
|
||||
|
||||
if (requestInfo == null || requestInfo.IsLocalRequest || requestInfo.IsAnonymousRequest)
|
||||
return Task.FromResult(true);
|
||||
|
||||
try
|
||||
{
|
||||
var path = httpContext.Request.Path.Value ?? "";
|
||||
|
||||
// Configurable Regex filters with ReDoS protection (timeout 100ms)
|
||||
try
|
||||
{
|
||||
if (RegexFilters.Any(pattern => Regex.IsMatch(path, pattern, RegexOptions.None, TimeSpan.FromMilliseconds(100))))
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
// ReDoS protection triggered, skip this request and log warning
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
// Extension filter
|
||||
if (SkipExtensions.Any(ext => path.EndsWith(ext, StringComparison.OrdinalIgnoreCase)))
|
||||
return Task.FromResult(true);
|
||||
|
||||
// Prefix filter
|
||||
if (SkipPrefixes.Any(prefix => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
|
||||
return Task.FromResult(true);
|
||||
|
||||
var realIP = requestInfo.IP;
|
||||
if (string.IsNullOrEmpty(realIP) || realIP == "127.0.0.1" || realIP == "::1")
|
||||
return Task.FromResult(true);
|
||||
|
||||
// IP Blacklist check
|
||||
if (IsBlacklistedIp(realIP))
|
||||
return Task.FromResult(true);
|
||||
|
||||
// Rate limit
|
||||
if (RateLimitMs > 0)
|
||||
{
|
||||
var rateLimitKey = $"{realIP}:{path}";
|
||||
if (_rateLimitCache.TryGetValue(rateLimitKey, out _))
|
||||
return Task.FromResult(true);
|
||||
|
||||
_rateLimitCache.Set(rateLimitKey, true, new MemoryCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(RateLimitMs),
|
||||
Size = 1
|
||||
});
|
||||
}
|
||||
|
||||
string userAgent = httpContext.Request.Headers["User-Agent"].FirstOrDefault() ?? "unknown";
|
||||
if (userAgent.Length > 1024) userAgent = userAgent[..1024];
|
||||
|
||||
string userUid = httpContext.Request.Query["uid"].FirstOrDefault() ?? httpContext.Request.Cookies["uid"] ?? "anonymous";
|
||||
if (userUid.Length > 256) userUid = userUid[..256];
|
||||
|
||||
var cacheKey = $"{realIP}:{userAgent}";
|
||||
if (!_userIdCache.TryGetValue(cacheKey, out string? userId))
|
||||
{
|
||||
userId = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(cacheKey))).ToLower();
|
||||
_userIdCache.Set(cacheKey, userId, new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromHours(2), Size = 1 });
|
||||
}
|
||||
|
||||
var user = new UserInfoModelSql
|
||||
{
|
||||
Id = userId!,
|
||||
Ip = realIP,
|
||||
UserAgent = userAgent
|
||||
};
|
||||
|
||||
var fullUri = path + httpContext.Request.QueryString;
|
||||
if (fullUri.Length > 2048) fullUri = fullUri[..2048];
|
||||
|
||||
string balancer = "";
|
||||
if (path.StartsWith("/lite/")) balancer = path[6..].Split('/', '?')[0];
|
||||
else if (path.StartsWith("/rc/")) balancer = path[4..].Split('/', '?')[0];
|
||||
else
|
||||
{
|
||||
// Parse 18+ (sisi) modules which sit at the root
|
||||
string[] adultRoutes = { "bgs", "chu", "elo", "epr", "hqr", "phub", "phubgay", "phubsml", "phubprem", "ptx", "runetki", "sbg", "tizam", "xmr", "xmrgay", "xmrsml", "xnx", "xds", "xdsgay", "xdssml", "xdsred", "nexthub" };
|
||||
var rootSegment = path.TrimStart('/').Split('/', '?')[0].ToLowerInvariant();
|
||||
|
||||
if (adultRoutes.Contains(rootSegment))
|
||||
{
|
||||
balancer = "18+ " + rootSegment;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out system endpoints that are not actual movie balancers
|
||||
string[] systemEndpoints = { "events", "sync", "auth", "ws", "subs", "vtt", "search", "suggest", "torrent", "tracker", "info", "mdl" };
|
||||
if (systemEndpoints.Contains(balancer.ToLowerInvariant()))
|
||||
{
|
||||
balancer = "";
|
||||
}
|
||||
|
||||
// Deduplication for balancers (First + Last within 30 seconds)
|
||||
if (!string.IsNullOrEmpty(balancer))
|
||||
{
|
||||
var dedupKey = $"{userUid}:{balancer}";
|
||||
var lastSeen = _balancerDedupCache.Get<DateTime?>(dedupKey);
|
||||
|
||||
if (lastSeen.HasValue && (DateTime.UtcNow - lastSeen.Value).TotalSeconds < 30)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_balancerDedupCache.Set(dedupKey, DateTime.UtcNow, new MemoryCacheEntryOptions
|
||||
{
|
||||
SlidingExpiration = TimeSpan.FromSeconds(30),
|
||||
Size = 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
int durationMs = 0;
|
||||
if (httpContext.Items.TryGetValue("Telemetry_StartTime", out var startTimeObj) && startTimeObj is DateTime startTime)
|
||||
{
|
||||
durationMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
}
|
||||
|
||||
string? mTitle = null, mTmdb = null, mKp = null, mImdb = null;
|
||||
bool mIsTv = false;
|
||||
|
||||
var reqQuery = httpContext.Request.Query;
|
||||
if (reqQuery.TryGetValue("title", out var titleVal)) mTitle = titleVal.ToString();
|
||||
if (reqQuery.TryGetValue("id", out var idVal)) mTmdb = idVal.ToString();
|
||||
if (reqQuery.TryGetValue("kinopoisk_id", out var kpVal)) mKp = kpVal.ToString();
|
||||
if (reqQuery.TryGetValue("imdb_id", out var imdbVal)) mImdb = imdbVal.ToString();
|
||||
if (reqQuery.TryGetValue("serial", out var serialVal)) mIsTv = serialVal.ToString() == "1" || serialVal.ToString().Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var log = new LogModelSql
|
||||
{
|
||||
Time = DateTime.UtcNow,
|
||||
Uri = fullUri,
|
||||
Uid = userUid,
|
||||
UnfoId = user.Id,
|
||||
DurationMs = durationMs,
|
||||
Balancer = balancer,
|
||||
StatusCode = httpContext.Response?.StatusCode ?? 0,
|
||||
MovieTitle = mTitle,
|
||||
TmdbId = mTmdb,
|
||||
KpId = mKp,
|
||||
ImdbId = mImdb,
|
||||
IsTv = mIsTv
|
||||
};
|
||||
|
||||
if (Interlocked.Increment(ref _queueSize) <= MaxQueueSize)
|
||||
{
|
||||
try { Queue.Enqueue((log, user)); }
|
||||
catch { Interlocked.Decrement(ref _queueSize); }
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Decrement(ref _queueSize);
|
||||
// Dropped
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public static void SaveSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(_settingsPath);
|
||||
if (dir != null) Directory.CreateDirectory(dir);
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["prefixes"] = SkipPrefixes,
|
||||
["extensions"] = SkipExtensions,
|
||||
["regexes"] = RegexFilters,
|
||||
["ips"] = BlacklistIps,
|
||||
["rateLimit"] = RateLimitMs
|
||||
};
|
||||
File.WriteAllText(_settingsPath, System.Text.Json.JsonSerializer.Serialize(data, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Telemetry] SaveSettings Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void LoadSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_settingsPath)) return;
|
||||
var json = File.ReadAllText(_settingsPath);
|
||||
var doc = System.Text.Json.JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("prefixes", out var p)) SkipPrefixes = p.EnumerateArray().Select(e => e.GetString()!).ToArray();
|
||||
if (root.TryGetProperty("extensions", out var e2)) SkipExtensions = e2.EnumerateArray().Select(e => e.GetString()!).ToArray();
|
||||
if (root.TryGetProperty("regexes", out var r)) RegexFilters = r.EnumerateArray().Select(e => e.GetString()!).ToArray();
|
||||
if (root.TryGetProperty("ips", out var i)) BlacklistIps = i.EnumerateArray().Select(e => e.GetString()!).ToArray();
|
||||
if (root.TryGetProperty("rateLimit", out var rl)) RateLimitMs = rl.GetInt32();
|
||||
|
||||
Console.WriteLine("[Telemetry] Settings loaded from disk.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Telemetry] LoadSettings Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>Telemetry</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\Shared.csproj" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"enable": true,
|
||||
"dynamic": true,
|
||||
"tree": [
|
||||
"Controllers",
|
||||
"Models",
|
||||
"AppDbContext.cs",
|
||||
"ModInit.cs",
|
||||
"RequestListener.cs"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user