diff --git a/Aberwyn/Controllers/TorrentController.cs b/Aberwyn/Controllers/TorrentController.cs index 9cabed5..8029b88 100644 --- a/Aberwyn/Controllers/TorrentController.cs +++ b/Aberwyn/Controllers/TorrentController.cs @@ -12,9 +12,10 @@ public class TorrentController : Controller } [HttpGet] - public IActionResult Index() + public async Task Index() { - return View(new TorrentUploadViewModel()); + var torrents = await _torrentService.GetRecentTorrentsAsync(50); + return View(torrents); // skicka lista av TorrentItem } [HttpPost] @@ -96,4 +97,19 @@ public class TorrentController : Controller return Json(new { success = false, error = "Fel vid uppdatering" }); } } + [HttpGet] + public async Task List() + { + try + { + var torrents = await _torrentService.GetRecentTorrentsAsync(50); + return View(torrents); + } + catch (Exception ex) + { + _logger.LogError(ex, "Fel vid hämtning av torrent-lista"); + return View(new List()); + } + } + } \ No newline at end of file diff --git a/Aberwyn/Data/ApplicationDbContext.cs b/Aberwyn/Data/ApplicationDbContext.cs index 869840d..edd3b9f 100644 --- a/Aberwyn/Data/ApplicationDbContext.cs +++ b/Aberwyn/Data/ApplicationDbContext.cs @@ -14,7 +14,13 @@ namespace Aberwyn.Data protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); + builder.Entity() + .HasIndex(t => t.InfoHash) + .IsUnique(); + builder.Entity() + .Property(t => t.Status) + .HasConversion(); builder.Entity().ToTable("WeeklyMenu"); builder.Entity().HasData( new MealCategory @@ -52,7 +58,9 @@ namespace Aberwyn.Data public DbSet LabIngredients { get; set; } public DbSet LabVersionIngredients { get; set; } public DbSet MealRatings { get; set; } - + public DbSet TorrentItems { get; set; } + public DbSet RssFeeds { get; set; } + public DbSet DownloadRules { get; set; } } } diff --git a/Aberwyn/Data/HDTorrentsTrackerScraper.cs b/Aberwyn/Data/HDTorrentsTrackerScraper.cs new file mode 100644 index 0000000..479b8ff --- /dev/null +++ b/Aberwyn/Data/HDTorrentsTrackerScraper.cs @@ -0,0 +1,103 @@ +using System.Text; + +namespace Aberwyn.Data +{ + public class HdTorrentsTrackerScraper + { + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public HdTorrentsTrackerScraper(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public async Task<(int seeders, int leechers, int completed)> ScrapeHdTorrents(string infoHash, string downloadKey) + { + try + { + if (string.IsNullOrEmpty(infoHash) || string.IsNullOrEmpty(downloadKey)) + return (0, 0, 0); + + // Konvertera hex InfoHash till URL-encoded bytes + var infoHashBytes = HexStringToBytes(infoHash); + + //%67%2e%d6%f9%30%e9%33%88%e3%1b%c6%f0%85%f5%f7%78%44%05%10%e7 + var encodedInfoHash = Uri.EscapeDataString(Encoding.GetEncoding("ISO-8859-1").GetString(infoHashBytes)); + var encodedInfoHashv2 = EncodeInfoHash(infoHash); + + // Bygga scrape URL baserat på HD-Torrents format + var scrapeUrl = $"https://hdts-announce.ru/scrape.php?pid=98d498dedff78ba0334f662d151eb19b7?info_hash={encodedInfoHashv2}"; + + _logger.LogInformation("Scraping HD-Torrents: {Url}", scrapeUrl); + + var response = await _httpClient.GetAsync(scrapeUrl); + + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsByteArrayAsync(); // Fix: Content.ReadAsByteArrayAsync() + return ParseBencodeResponse(content); + } + else + { + _logger.LogWarning("Scrape failed with status: {StatusCode}", response.StatusCode); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to scrape HD-Torrents for InfoHash: {InfoHash}", infoHash); + } + + return (0, 0, 0); + } + private static string EncodeInfoHash(string hex) + { + var bytes = HexStringToBytes(hex); + return string.Concat(bytes.Select(b => $"%{b:X2}")).ToLower(); + } + private static byte[] HexStringToBytes(string hex) + { + if (hex.Length % 2 != 0) + throw new ArgumentException("Invalid hex string length."); + + var bytes = new byte[hex.Length / 2]; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); + } + return bytes; + } + + private (int seeders, int leechers, int completed) ParseBencodeResponse(byte[] data) + { + try + { + // Enkel bencode parsing för tracker response + var response = System.Text.Encoding.UTF8.GetString(data); + + // Tracker response är vanligtvis i format: + // d5:filesd20:[info_hash]d8:completei[antal]e9:downloadedi[antal]e10:incompletei[antal]eee + + // Hitta complete (seeders) + var completeMatch = System.Text.RegularExpressions.Regex.Match(response, @"8:completei(\d+)e"); + var seeders = completeMatch.Success ? int.Parse(completeMatch.Groups[1].Value) : 0; + + // Hitta incomplete (leechers) + var incompleteMatch = System.Text.RegularExpressions.Regex.Match(response, @"10:incompletei(\d+)e"); + var leechers = incompleteMatch.Success ? int.Parse(incompleteMatch.Groups[1].Value) : 0; + + // Hitta downloaded (completed) + var downloadedMatch = System.Text.RegularExpressions.Regex.Match(response, @"9:downloadedi(\d+)e"); + var completed = downloadedMatch.Success ? int.Parse(downloadedMatch.Groups[1].Value) : 0; + + return (seeders, leechers, completed); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to parse bencode response"); + return (0, 0, 0); + } + } + } +} diff --git a/Aberwyn/Data/IRssProcessor.cs b/Aberwyn/Data/IRssProcessor.cs new file mode 100644 index 0000000..2d36ae3 --- /dev/null +++ b/Aberwyn/Data/IRssProcessor.cs @@ -0,0 +1,7 @@ +namespace Aberwyn.Data +{ + public interface IRssProcessor + { + Task ProcessRssFeeds(); + } +} diff --git a/Aberwyn/Data/RssProcessor.cs b/Aberwyn/Data/RssProcessor.cs new file mode 100644 index 0000000..6c65cf7 --- /dev/null +++ b/Aberwyn/Data/RssProcessor.cs @@ -0,0 +1,286 @@ +using Aberwyn.Data; +using Aberwyn.Models; +using Microsoft.EntityFrameworkCore; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace Aberwyn.Data +{ + public class RssProcessor : IRssProcessor + { + private readonly ApplicationDbContext _context; + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + private readonly HdTorrentsTrackerScraper _trackerScraper; + + public RssProcessor(ApplicationDbContext context, ILogger logger, + HttpClient httpClient, HdTorrentsTrackerScraper trackerScraper) + { + _context = context; + _logger = logger; + _httpClient = httpClient; + _trackerScraper = trackerScraper; + } + + public async Task ProcessRssFeeds() + { + var oneHourAgo = DateTime.UtcNow.AddHours(-1); + + var activeFeeds = await _context.RssFeeds + .Where(f => f.IsActive && f.LastChecked <= oneHourAgo) + .ToListAsync(); + + foreach (var feed in activeFeeds) + { + try + { + await ProcessSingleFeed(feed); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing RSS feed {FeedName}", feed.Name); + } + } + } + + private async Task ProcessSingleFeed(RssFeed feed) + { + var rssContent = await _httpClient.GetStringAsync(feed.Url); + var rssDoc = XDocument.Parse(rssContent); + var items = rssDoc.Descendants("item"); + try + { + foreach (var item in items) + { + try + { + var torrentItem = ParseRssItem(item, feed); + Console.WriteLine($"Trying to save: Title='{torrentItem.Title}', InfoHash='{torrentItem.InfoHash}', MovieName='{torrentItem.MovieName}'"); + + // Kolla om den redan finns + var exists = await _context.TorrentItems + .AnyAsync(t => t.InfoHash == torrentItem.InfoHash || + (t.Title == torrentItem.Title && t.RssSource == feed.Name)); + + + if (!exists) + { + if (!string.IsNullOrEmpty(torrentItem.InfoHash) && !string.IsNullOrEmpty(torrentItem.DownloadKey)) + { + var (seeders, leechers, completed) = await _trackerScraper.ScrapeHdTorrents( + torrentItem.InfoHash, + torrentItem.DownloadKey); + + torrentItem.Seeders = seeders; + torrentItem.Leechers = leechers; + torrentItem.Completed = completed; + + Console.WriteLine($"Scraped stats for {torrentItem.Title}: S:{seeders} L:{leechers} C:{completed}"); + } + _context.TorrentItems.Add(torrentItem); + var savedChanges = await _context.SaveChangesAsync(); + Console.WriteLine($"SaveChanges returned: {savedChanges}"); + // Kolla auto-download regler + if (await ShouldAutoDownload(torrentItem)) + { + await ProcessTorrentDownload(torrentItem); + } + } else + { + var howLongAgo = DateTime.UtcNow.AddHours(-6); + if (torrentItem.PublishDate >= howLongAgo) + { + + var existing = await _context.TorrentItems + .FirstOrDefaultAsync(t => t.InfoHash == torrentItem.InfoHash + || (t.Title == torrentItem.Title && t.RssSource == feed.Name)); + + if (existing != null) + { + var (seeders, leechers, completed) = await _trackerScraper.ScrapeHdTorrents( + existing.InfoHash, + existing.DownloadKey); + + existing.Seeders = seeders; + existing.Leechers = leechers; + existing.Completed = completed; + + _context.TorrentItems.Update(existing); + await _context.SaveChangesAsync(); + + Console.WriteLine($"Updated stats for {existing.Title}: S:{seeders} L:{leechers} C:{completed}"); + } + } + + } + } + catch (Exception itemEx) + { + Console.WriteLine($"Error processing individual item: {itemEx.Message}"); + Console.WriteLine($"Stack trace: {itemEx.StackTrace}"); + } + } + + feed.LastChecked = DateTime.UtcNow; + await _context.SaveChangesAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"Error in ProcessSingleFeed: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + } + } + + private TorrentItem ParseRssItem(XElement item, RssFeed feed) + { + var title = item.Element("title")?.Value ?? "Unknown Title"; + var description = item.Element("description")?.Value ?? ""; // Fix: Default till tom sträng + var pubDate = DateTime.TryParse(item.Element("pubDate")?.Value, out var date) ? date : DateTime.UtcNow; + var link = item.Element("link")?.Value ?? ""; + + var infoHash = ExtractInfoHashFromUrl(link); + var downloadKey = ExtractParameterFromUrl(link, "key"); + var token = ExtractParameterFromUrl(link, "token"); + var (movieName, year) = ParseMovieNameAndYear(title); + + var magnetLink = ""; + if (!string.IsNullOrEmpty(infoHash)) + { + magnetLink = $"magnet:?xt=urn:btih:{infoHash}&dn={Uri.EscapeDataString(title)}"; + } + + return new TorrentItem + { + Title = title ?? "Unknown Title", // Garanterat inte null + Description = description ?? string.Empty, // Garanterat inte null + TorrentUrl = string.IsNullOrEmpty(link) ? null : link, + MagnetLink = string.IsNullOrEmpty(magnetLink) ? null : magnetLink, + InfoHash = string.IsNullOrEmpty(infoHash) ? null : infoHash, + PublishDate = pubDate, + RssSource = feed.Name ?? "Unknown", // Garanterat inte null + Status = TorrentStatus.New, + DownloadKey = string.IsNullOrEmpty(downloadKey) ? null : downloadKey, + Token = string.IsNullOrEmpty(token) ? null : token, + MovieName = string.IsNullOrEmpty(movieName) ? null : movieName, + Year = year, + Category = DetermineCategory(title), + CreatedAt = DateTime.UtcNow + }; + } + + private (string movieName, int? year) ParseMovieNameAndYear(string title) + { + // Exempel titlar: + // "Bring It on Fight to the Finish 2009 BluRay 1080p DDP 5.1 x264-hallowed" + // "Deadpool & Wolverine 2024 Hybrid 1080p UHD BluRay DD+5.1 Atmos DV HDR10+ x265-HiDt" + + var movieName = ""; + int? year = null; + + // Leta efter år (4 siffror mellan 1900-2099) + var yearMatch = System.Text.RegularExpressions.Regex.Match(title, @"\b(19\d{2}|20\d{2})\b"); + if (yearMatch.Success && int.TryParse(yearMatch.Groups[1].Value, out var parsedYear)) + { + year = parsedYear; + + // Ta allt före året som filmnamn + var yearIndex = yearMatch.Index; + movieName = title.Substring(0, yearIndex).Trim(); + } + else + { + // Om inget år hittas, ta första delen före vanliga release-keywords + var keywordMatch = System.Text.RegularExpressions.Regex.Match(title, + @"\b(BluRay|WEB-DL|WEBRip|HDTV|DVDRIP|REMUX|UHD|1080p|720p|480p|2160p)\b", + RegexOptions.IgnoreCase); + + if (keywordMatch.Success) + { + movieName = title.Substring(0, keywordMatch.Index).Trim(); + } + else + { + // Fallback: ta första delen innan sista bindestreck + var lastDashIndex = title.LastIndexOf('-'); + if (lastDashIndex > 0) + { + movieName = title.Substring(0, lastDashIndex).Trim(); + } + else + { + movieName = title; + } + } + } + + // Rensa bort vanliga suffix från filmnamnet + movieName = System.Text.RegularExpressions.Regex.Replace(movieName, + @"\b(REMASTERED|REPACK|PROPER|REAL|EXTENDED|DIRECTORS?\.?CUT|UNRATED)\b", + "", RegexOptions.IgnoreCase).Trim(); + + return (movieName, year); + } + + private string ExtractInfoHashFromUrl(string url) + { + if (string.IsNullOrEmpty(url)) return ""; + + var match = System.Text.RegularExpressions.Regex.Match(url, @"hash=([a-fA-F0-9]{40})"); + return match.Success ? match.Groups[1].Value.ToUpper() : ""; + } + + private string ExtractParameterFromUrl(string url, string paramName) + { + if (string.IsNullOrEmpty(url)) return ""; + + var match = System.Text.RegularExpressions.Regex.Match(url, $@"{paramName}=([^&]+)"); + return match.Success ? Uri.UnescapeDataString(match.Groups[1].Value) : ""; + } + + private string DetermineCategory(string title) + { + if (System.Text.RegularExpressions.Regex.IsMatch(title, @"\b(S\d{2}E\d{2}|Season|Episode)\b", RegexOptions.IgnoreCase)) + return "TV"; + else + return "Movies"; + } + + private async Task ShouldAutoDownload(TorrentItem item) + { + var rules = await _context.DownloadRules.Where(r => r.AutoDownload).ToListAsync(); + + return rules.Any(rule => + (string.IsNullOrEmpty(rule.KeywordFilter) || + item.Title.Contains(rule.KeywordFilter, StringComparison.OrdinalIgnoreCase)) && + (string.IsNullOrEmpty(rule.CategoryFilter) || + item.Category == rule.CategoryFilter) && + item.Seeders >= rule.MinSeeders && + (rule.MaxSize == 0 || item.Size <= rule.MaxSize)); + } + + private async Task ProcessTorrentDownload(TorrentItem item) + { + try + { + if (!string.IsNullOrEmpty(item.MagnetLink)) + { + _logger.LogInformation("Starting magnet download for {Title}", item.Title); + item.Status = TorrentStatus.Downloaded; + } + else if (!string.IsNullOrEmpty(item.TorrentUrl)) + { + var torrentData = await _httpClient.GetByteArrayAsync(item.TorrentUrl); + item.Status = TorrentStatus.Downloaded; + } + + await _context.SaveChangesAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error downloading torrent {Title}", item.Title); + item.Status = TorrentStatus.Failed; + await _context.SaveChangesAsync(); + } + } + } +} \ No newline at end of file diff --git a/Aberwyn/Data/TorrentRssService.cs b/Aberwyn/Data/TorrentRssService.cs new file mode 100644 index 0000000..6acd484 --- /dev/null +++ b/Aberwyn/Data/TorrentRssService.cs @@ -0,0 +1,41 @@ +using Aberwyn.Data; + +namespace Aberwyn.Services +{ + public class TorrentRssService : BackgroundService + { + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public TorrentRssService(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Vänta lite innan första körningen + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + using var scope = _serviceProvider.CreateScope(); + var rssProcessor = scope.ServiceProvider.GetRequiredService(); + await rssProcessor.ProcessRssFeeds(); + + _logger.LogInformation("RSS feeds processed successfully at {Time}", DateTime.UtcNow); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing RSS feeds"); + } + + // Vänta 10 minuter innan nästa körning + await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken); + } + } + } +} \ No newline at end of file diff --git a/Aberwyn/Data/TorrentService.cs b/Aberwyn/Data/TorrentService.cs index af9bf64..b9e5b38 100644 --- a/Aberwyn/Data/TorrentService.cs +++ b/Aberwyn/Data/TorrentService.cs @@ -1,19 +1,23 @@ +using Aberwyn.Data; +using BencodeNET.Objects; using BencodeNET.Parsing; using BencodeNET.Torrents; -using BencodeNET.Objects; +using Microsoft.EntityFrameworkCore; using System.Text; public interface ITorrentService { Task ParseTorrentAsync(IFormFile file); Task FetchTrackerStatsAsync(TorrentInfo info); + Task> GetRecentTorrentsAsync(int count); } public class TorrentService : ITorrentService { private readonly HttpClient _httpClient; private readonly ILogger _logger; - + private readonly ApplicationDbContext _context; + // Kända trackers och deras egenskaper private readonly Dictionary _knownTrackers = new() { @@ -27,13 +31,21 @@ public class TorrentService : ITorrentService } }; - public TorrentService(HttpClient httpClient, ILogger logger) + public TorrentService(HttpClient httpClient, ILogger logger, ApplicationDbContext context) { _httpClient = httpClient; _logger = logger; + _context = context; _httpClient.Timeout = TimeSpan.FromSeconds(10); } + public async Task> GetRecentTorrentsAsync(int count) + { + return await _context.TorrentItems + .OrderByDescending(t => t.PublishDate) + .Take(count) + .ToListAsync(); + } public async Task ParseTorrentAsync(IFormFile file) { try @@ -87,18 +99,32 @@ public class TorrentService : ITorrentService if (bdict.TryGetValue("files", out var filesValue) && filesValue is BDictionary files) { - if (TryGetStatsFromFiles(files, info.InfoHash, info) || - TryGetStatsFromFiles(files, Encoding.UTF8.GetString(info.InfoHashBytes), info)) + // Använd direkt byte array istället för att konvertera till sträng + if (TryGetStatsFromFiles(files, info.InfoHashBytes, info)) { info.HasTrackerData = true; return info; } + + // Om det inte fungerar, prova att URL-decode först + if (!string.IsNullOrEmpty(info.InfoHash)) + { + try + { + string decoded = Uri.UnescapeDataString(info.InfoHash); + byte[] decodedBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(decoded); + + if (TryGetStatsFromFiles(files, decodedBytes, info)) + { + info.HasTrackerData = true; + return info; + } + } + catch { /* Ignore decode errors */ } + } + info.ErrorMessage = "Info hash hittades inte i tracker-svaret"; } - else - { - info.ErrorMessage = "Inget 'files' objekt i tracker-svaret"; - } } catch (HttpRequestException ex) { @@ -118,10 +144,21 @@ public class TorrentService : ITorrentService return info; } - - private bool TryGetStatsFromFiles(BDictionary files, string hashKey, TorrentInfo info) + private bool ByteArraysEqual(byte[] a, byte[] b) { - if (files.TryGetValue(hashKey, out var hashEntry) && hashEntry is BDictionary stats) + if (a.Length != b.Length) return false; + for (int i = 0; i < a.Length; i++) + { + if (a[i] != b[i]) return false; + } + return true; + } + private bool TryGetStatsFromFiles(BDictionary files, byte[] hashBytes, TorrentInfo info) + { + // Skapa en BString från byte array + var bStringKey = new BString(hashBytes); + + if (files.TryGetValue(bStringKey, out var hashEntry) && hashEntry is BDictionary stats) { info.Seeders = stats.TryGetInt("complete") ?? 0; info.Leechers = stats.TryGetInt("incomplete") ?? 0; diff --git a/Aberwyn/Migrations/20250805202224_AddTorrentTables.Designer.cs b/Aberwyn/Migrations/20250805202224_AddTorrentTables.Designer.cs new file mode 100644 index 0000000..bd774ed --- /dev/null +++ b/Aberwyn/Migrations/20250805202224_AddTorrentTables.Designer.cs @@ -0,0 +1,1177 @@ +// +using System; +using Aberwyn.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aberwyn.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250805202224_AddTorrentTables")] + partial class AddTorrentTables + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.36") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Aberwyn.Models.AppSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("AppSettings"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BudgetCategoryDefinitionId") + .HasColumnType("int"); + + b.Property("BudgetPeriodId") + .HasColumnType("int"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BudgetCategoryDefinitionId"); + + b.HasIndex("BudgetPeriodId"); + + b.ToTable("BudgetCategories"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategoryDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetCategoryDefinitions"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("BudgetCategoryId") + .HasColumnType("int"); + + b.Property("BudgetItemDefinitionId") + .HasColumnType("int"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BudgetCategoryId"); + + b.HasIndex("BudgetItemDefinitionId"); + + b.ToTable("BudgetItems"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultCategory") + .HasColumnType("longtext"); + + b.Property("DefaultPaymentStatus") + .HasColumnType("int"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetItemDefinitions"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Month") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("BudgetPeriods"); + }); + + modelBuilder.Entity("Aberwyn.Models.Ingredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MealId") + .HasColumnType("int"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("MealId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipeLabEntryId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabEntryId"); + + b.ToTable("LabIngredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabVersionIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipeLabVersionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabVersionId"); + + b.ToTable("LabVersionIngredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.Meal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CarbType") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ImageData") + .HasColumnType("longblob"); + + b.Property("ImageMimeType") + .HasColumnType("longtext"); + + b.Property("ImageUrl") + .HasColumnType("longtext"); + + b.Property("Instructions") + .HasColumnType("longtext"); + + b.Property("IsAvailable") + .HasColumnType("tinyint(1)"); + + b.Property("IsPublished") + .HasColumnType("tinyint(1)"); + + b.Property("MealCategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProteinType") + .HasColumnType("longtext"); + + b.Property("RecipeUrl") + .HasColumnType("longtext"); + + b.Property("ThumbnailData") + .HasColumnType("longblob"); + + b.HasKey("Id"); + + b.HasIndex("MealCategoryId"); + + b.ToTable("Meals"); + }); + + modelBuilder.Entity("Aberwyn.Models.MealCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Color") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("Icon") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Slug") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("MealCategories"); + + b.HasData( + new + { + Id = 1, + Color = "#f97316", + DisplayOrder = 1, + Icon = "🍕", + IsActive = true, + Name = "Pizza", + Slug = "pizza" + }); + }); + + modelBuilder.Entity("Aberwyn.Models.MealRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("MealId") + .HasColumnType("int"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("MealId"); + + b.ToTable("MealRatings"); + }); + + modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CustomerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IngredientsJson") + .HasColumnType("longtext"); + + b.Property("OrderedAt") + .HasColumnType("datetime(6)"); + + b.Property("PizzaName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PizzaOrders"); + }); + + modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("P256DH") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PizzaOrderId") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PizzaOrderId"); + + b.HasIndex("UserId"); + + b.ToTable("PushSubscribers"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BaseMealId") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Inspiration") + .HasColumnType("longtext"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("Tags") + .HasColumnType("longtext"); + + b.Property("TestedBy") + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BaseMealId"); + + b.ToTable("RecipeLabEntries"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Instructions") + .HasColumnType("longtext"); + + b.Property("RecipeLabEntryId") + .HasColumnType("int"); + + b.Property("ResultNotes") + .HasColumnType("longtext"); + + b.Property("VersionLabel") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabEntryId"); + + b.ToTable("RecipeLabVersions"); + }); + + modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("P256DH") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PushSubscriptions"); + }); + + modelBuilder.Entity("Aberwyn.Models.TodoTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AssignedTo") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("TodoTasks"); + }); + + modelBuilder.Entity("Aberwyn.Models.UserPreferences", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("NotifyBudget") + .HasColumnType("tinyint(1)"); + + b.Property("NotifyMenu") + .HasColumnType("tinyint(1)"); + + b.Property("NotifyPizza") + .HasColumnType("tinyint(1)"); + + b.HasKey("UserId"); + + b.ToTable("UserPreferences"); + }); + + modelBuilder.Entity("Aberwyn.Models.WeeklyMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BreakfastMealId") + .HasColumnType("int"); + + b.Property("Cook") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DayOfWeek") + .HasColumnType("int"); + + b.Property("DinnerMealId") + .HasColumnType("int"); + + b.Property("LunchMealId") + .HasColumnType("int"); + + b.Property("WeekNumber") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("WeeklyMenu", (string)null); + }); + + modelBuilder.Entity("DownloadRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AutoDownload") + .HasColumnType("tinyint(1)"); + + b.Property("CategoryFilter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("KeywordFilter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxSize") + .HasColumnType("bigint"); + + b.Property("MinSeeders") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("DownloadRules"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("RssFeed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastChecked") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("RssFeeds"); + }); + + modelBuilder.Entity("TorrentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Category") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InfoHash") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Leechers") + .HasColumnType("int"); + + b.Property("MagnetLink") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PublishDate") + .HasColumnType("datetime(6)"); + + b.Property("RssSource") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Seeders") + .HasColumnType("int"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TorrentUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("InfoHash") + .IsUnique(); + + b.ToTable("TorrentItems"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition") + .WithMany() + .HasForeignKey("BudgetCategoryDefinitionId"); + + b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod") + .WithMany("Categories") + .HasForeignKey("BudgetPeriodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BudgetPeriod"); + + b.Navigation("Definition"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => + { + b.HasOne("Aberwyn.Models.BudgetCategory", null) + .WithMany("Items") + .HasForeignKey("BudgetCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition") + .WithMany() + .HasForeignKey("BudgetItemDefinitionId"); + + b.Navigation("BudgetItemDefinition"); + }); + + modelBuilder.Entity("Aberwyn.Models.Ingredient", b => + { + b.HasOne("Aberwyn.Models.Meal", null) + .WithMany("Ingredients") + .HasForeignKey("MealId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Aberwyn.Models.LabIngredient", b => + { + b.HasOne("Aberwyn.Models.RecipeLabEntry", "Entry") + .WithMany("Ingredients") + .HasForeignKey("RecipeLabEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabVersionIngredient", b => + { + b.HasOne("Aberwyn.Models.RecipeLabVersion", "Version") + .WithMany("Ingredients") + .HasForeignKey("RecipeLabVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Version"); + }); + + modelBuilder.Entity("Aberwyn.Models.Meal", b => + { + b.HasOne("Aberwyn.Models.MealCategory", "Category") + .WithMany("Meals") + .HasForeignKey("MealCategoryId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Aberwyn.Models.MealRating", b => + { + b.HasOne("Aberwyn.Models.Meal", "Meal") + .WithMany() + .HasForeignKey("MealId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Meal"); + }); + + modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b => + { + b.HasOne("Aberwyn.Models.PizzaOrder", "PizzaOrder") + .WithMany() + .HasForeignKey("PizzaOrderId"); + + b.HasOne("Aberwyn.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PizzaOrder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.HasOne("Aberwyn.Models.Meal", "BaseMeal") + .WithMany() + .HasForeignKey("BaseMealId"); + + b.Navigation("BaseMeal"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.HasOne("Aberwyn.Models.RecipeLabEntry", "Entry") + .WithMany("Versions") + .HasForeignKey("RecipeLabEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aberwyn.Models.UserPreferences", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", "User") + .WithOne("Preferences") + .HasForeignKey("Aberwyn.Models.UserPreferences", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b => + { + b.Navigation("Preferences") + .IsRequired(); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("Aberwyn.Models.Meal", b => + { + b.Navigation("Ingredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.MealCategory", b => + { + b.Navigation("Meals"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.Navigation("Ingredients"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.Navigation("Ingredients"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aberwyn/Migrations/20250805202224_AddTorrentTables.cs b/Aberwyn/Migrations/20250805202224_AddTorrentTables.cs new file mode 100644 index 0000000..26f207a --- /dev/null +++ b/Aberwyn/Migrations/20250805202224_AddTorrentTables.cs @@ -0,0 +1,105 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aberwyn.Migrations +{ + public partial class AddTorrentTables : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DownloadRules", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + KeywordFilter = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CategoryFilter = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + MinSeeders = table.Column(type: "int", nullable: false), + MaxSize = table.Column(type: "bigint", nullable: false), + AutoDownload = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DownloadRules", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "RssFeeds", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Url = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Name = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + LastChecked = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RssFeeds", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "TorrentItems", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Title = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + MagnetLink = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + InfoHash = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + PublishDate = table.Column(type: "datetime(6)", nullable: false), + Size = table.Column(type: "bigint", nullable: false), + Seeders = table.Column(type: "int", nullable: false), + Leechers = table.Column(type: "int", nullable: false), + Status = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Category = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + RssSource = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Description = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TorrentUrl = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_TorrentItems", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_TorrentItems_InfoHash", + table: "TorrentItems", + column: "InfoHash", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DownloadRules"); + + migrationBuilder.DropTable( + name: "RssFeeds"); + + migrationBuilder.DropTable( + name: "TorrentItems"); + } + } +} diff --git a/Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.Designer.cs b/Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.Designer.cs new file mode 100644 index 0000000..dbc7ffb --- /dev/null +++ b/Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.Designer.cs @@ -0,0 +1,1195 @@ +// +using System; +using Aberwyn.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aberwyn.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250806065232_AddTorrentTablesv2")] + partial class AddTorrentTablesv2 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.36") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Aberwyn.Models.AppSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("AppSettings"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BudgetCategoryDefinitionId") + .HasColumnType("int"); + + b.Property("BudgetPeriodId") + .HasColumnType("int"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BudgetCategoryDefinitionId"); + + b.HasIndex("BudgetPeriodId"); + + b.ToTable("BudgetCategories"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategoryDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetCategoryDefinitions"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("BudgetCategoryId") + .HasColumnType("int"); + + b.Property("BudgetItemDefinitionId") + .HasColumnType("int"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BudgetCategoryId"); + + b.HasIndex("BudgetItemDefinitionId"); + + b.ToTable("BudgetItems"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultCategory") + .HasColumnType("longtext"); + + b.Property("DefaultPaymentStatus") + .HasColumnType("int"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetItemDefinitions"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Month") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("BudgetPeriods"); + }); + + modelBuilder.Entity("Aberwyn.Models.Ingredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MealId") + .HasColumnType("int"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("MealId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipeLabEntryId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabEntryId"); + + b.ToTable("LabIngredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabVersionIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipeLabVersionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabVersionId"); + + b.ToTable("LabVersionIngredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.Meal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CarbType") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ImageData") + .HasColumnType("longblob"); + + b.Property("ImageMimeType") + .HasColumnType("longtext"); + + b.Property("ImageUrl") + .HasColumnType("longtext"); + + b.Property("Instructions") + .HasColumnType("longtext"); + + b.Property("IsAvailable") + .HasColumnType("tinyint(1)"); + + b.Property("IsPublished") + .HasColumnType("tinyint(1)"); + + b.Property("MealCategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProteinType") + .HasColumnType("longtext"); + + b.Property("RecipeUrl") + .HasColumnType("longtext"); + + b.Property("ThumbnailData") + .HasColumnType("longblob"); + + b.HasKey("Id"); + + b.HasIndex("MealCategoryId"); + + b.ToTable("Meals"); + }); + + modelBuilder.Entity("Aberwyn.Models.MealCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Color") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("Icon") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Slug") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("MealCategories"); + + b.HasData( + new + { + Id = 1, + Color = "#f97316", + DisplayOrder = 1, + Icon = "🍕", + IsActive = true, + Name = "Pizza", + Slug = "pizza" + }); + }); + + modelBuilder.Entity("Aberwyn.Models.MealRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("MealId") + .HasColumnType("int"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("MealId"); + + b.ToTable("MealRatings"); + }); + + modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CustomerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IngredientsJson") + .HasColumnType("longtext"); + + b.Property("OrderedAt") + .HasColumnType("datetime(6)"); + + b.Property("PizzaName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PizzaOrders"); + }); + + modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("P256DH") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PizzaOrderId") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PizzaOrderId"); + + b.HasIndex("UserId"); + + b.ToTable("PushSubscribers"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BaseMealId") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Inspiration") + .HasColumnType("longtext"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("Tags") + .HasColumnType("longtext"); + + b.Property("TestedBy") + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BaseMealId"); + + b.ToTable("RecipeLabEntries"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Instructions") + .HasColumnType("longtext"); + + b.Property("RecipeLabEntryId") + .HasColumnType("int"); + + b.Property("ResultNotes") + .HasColumnType("longtext"); + + b.Property("VersionLabel") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabEntryId"); + + b.ToTable("RecipeLabVersions"); + }); + + modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("P256DH") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PushSubscriptions"); + }); + + modelBuilder.Entity("Aberwyn.Models.TodoTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AssignedTo") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("TodoTasks"); + }); + + modelBuilder.Entity("Aberwyn.Models.UserPreferences", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("NotifyBudget") + .HasColumnType("tinyint(1)"); + + b.Property("NotifyMenu") + .HasColumnType("tinyint(1)"); + + b.Property("NotifyPizza") + .HasColumnType("tinyint(1)"); + + b.HasKey("UserId"); + + b.ToTable("UserPreferences"); + }); + + modelBuilder.Entity("Aberwyn.Models.WeeklyMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BreakfastMealId") + .HasColumnType("int"); + + b.Property("Cook") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DayOfWeek") + .HasColumnType("int"); + + b.Property("DinnerMealId") + .HasColumnType("int"); + + b.Property("LunchMealId") + .HasColumnType("int"); + + b.Property("WeekNumber") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("WeeklyMenu", (string)null); + }); + + modelBuilder.Entity("DownloadRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AutoDownload") + .HasColumnType("tinyint(1)"); + + b.Property("CategoryFilter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("KeywordFilter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxSize") + .HasColumnType("bigint"); + + b.Property("MinSeeders") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("DownloadRules"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("RssFeed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastChecked") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("RssFeeds"); + }); + + modelBuilder.Entity("TorrentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Category") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Completed") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DownloadKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InfoHash") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Leechers") + .HasColumnType("int"); + + b.Property("MagnetLink") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MovieName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PublishDate") + .HasColumnType("datetime(6)"); + + b.Property("RssSource") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Seeders") + .HasColumnType("int"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TorrentUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InfoHash") + .IsUnique(); + + b.ToTable("TorrentItems"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition") + .WithMany() + .HasForeignKey("BudgetCategoryDefinitionId"); + + b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod") + .WithMany("Categories") + .HasForeignKey("BudgetPeriodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BudgetPeriod"); + + b.Navigation("Definition"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => + { + b.HasOne("Aberwyn.Models.BudgetCategory", null) + .WithMany("Items") + .HasForeignKey("BudgetCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition") + .WithMany() + .HasForeignKey("BudgetItemDefinitionId"); + + b.Navigation("BudgetItemDefinition"); + }); + + modelBuilder.Entity("Aberwyn.Models.Ingredient", b => + { + b.HasOne("Aberwyn.Models.Meal", null) + .WithMany("Ingredients") + .HasForeignKey("MealId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Aberwyn.Models.LabIngredient", b => + { + b.HasOne("Aberwyn.Models.RecipeLabEntry", "Entry") + .WithMany("Ingredients") + .HasForeignKey("RecipeLabEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabVersionIngredient", b => + { + b.HasOne("Aberwyn.Models.RecipeLabVersion", "Version") + .WithMany("Ingredients") + .HasForeignKey("RecipeLabVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Version"); + }); + + modelBuilder.Entity("Aberwyn.Models.Meal", b => + { + b.HasOne("Aberwyn.Models.MealCategory", "Category") + .WithMany("Meals") + .HasForeignKey("MealCategoryId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Aberwyn.Models.MealRating", b => + { + b.HasOne("Aberwyn.Models.Meal", "Meal") + .WithMany() + .HasForeignKey("MealId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Meal"); + }); + + modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b => + { + b.HasOne("Aberwyn.Models.PizzaOrder", "PizzaOrder") + .WithMany() + .HasForeignKey("PizzaOrderId"); + + b.HasOne("Aberwyn.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PizzaOrder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.HasOne("Aberwyn.Models.Meal", "BaseMeal") + .WithMany() + .HasForeignKey("BaseMealId"); + + b.Navigation("BaseMeal"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.HasOne("Aberwyn.Models.RecipeLabEntry", "Entry") + .WithMany("Versions") + .HasForeignKey("RecipeLabEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aberwyn.Models.UserPreferences", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", "User") + .WithOne("Preferences") + .HasForeignKey("Aberwyn.Models.UserPreferences", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b => + { + b.Navigation("Preferences") + .IsRequired(); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("Aberwyn.Models.Meal", b => + { + b.Navigation("Ingredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.MealCategory", b => + { + b.Navigation("Meals"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.Navigation("Ingredients"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.Navigation("Ingredients"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.cs b/Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.cs new file mode 100644 index 0000000..359b8b9 --- /dev/null +++ b/Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aberwyn.Migrations +{ + public partial class AddTorrentTablesv2 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Completed", + table: "TorrentItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "DownloadKey", + table: "TorrentItems", + type: "longtext", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "MovieName", + table: "TorrentItems", + type: "longtext", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Token", + table: "TorrentItems", + type: "longtext", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Year", + table: "TorrentItems", + type: "int", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Completed", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "DownloadKey", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "MovieName", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "Token", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "Year", + table: "TorrentItems"); + } + } +} diff --git a/Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.Designer.cs b/Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.Designer.cs new file mode 100644 index 0000000..40cbc29 --- /dev/null +++ b/Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.Designer.cs @@ -0,0 +1,1188 @@ +// +using System; +using Aberwyn.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aberwyn.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250806072233_MakeTorrentFieldsNullable")] + partial class MakeTorrentFieldsNullable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.36") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Aberwyn.Models.AppSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("AppSettings"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BudgetCategoryDefinitionId") + .HasColumnType("int"); + + b.Property("BudgetPeriodId") + .HasColumnType("int"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BudgetCategoryDefinitionId"); + + b.HasIndex("BudgetPeriodId"); + + b.ToTable("BudgetCategories"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategoryDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetCategoryDefinitions"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("BudgetCategoryId") + .HasColumnType("int"); + + b.Property("BudgetItemDefinitionId") + .HasColumnType("int"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BudgetCategoryId"); + + b.HasIndex("BudgetItemDefinitionId"); + + b.ToTable("BudgetItems"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultCategory") + .HasColumnType("longtext"); + + b.Property("DefaultPaymentStatus") + .HasColumnType("int"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetItemDefinitions"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Month") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("BudgetPeriods"); + }); + + modelBuilder.Entity("Aberwyn.Models.Ingredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MealId") + .HasColumnType("int"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("MealId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipeLabEntryId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabEntryId"); + + b.ToTable("LabIngredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabVersionIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipeLabVersionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabVersionId"); + + b.ToTable("LabVersionIngredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.Meal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CarbType") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ImageData") + .HasColumnType("longblob"); + + b.Property("ImageMimeType") + .HasColumnType("longtext"); + + b.Property("ImageUrl") + .HasColumnType("longtext"); + + b.Property("Instructions") + .HasColumnType("longtext"); + + b.Property("IsAvailable") + .HasColumnType("tinyint(1)"); + + b.Property("IsPublished") + .HasColumnType("tinyint(1)"); + + b.Property("MealCategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProteinType") + .HasColumnType("longtext"); + + b.Property("RecipeUrl") + .HasColumnType("longtext"); + + b.Property("ThumbnailData") + .HasColumnType("longblob"); + + b.HasKey("Id"); + + b.HasIndex("MealCategoryId"); + + b.ToTable("Meals"); + }); + + modelBuilder.Entity("Aberwyn.Models.MealCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Color") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("Icon") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Slug") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("MealCategories"); + + b.HasData( + new + { + Id = 1, + Color = "#f97316", + DisplayOrder = 1, + Icon = "🍕", + IsActive = true, + Name = "Pizza", + Slug = "pizza" + }); + }); + + modelBuilder.Entity("Aberwyn.Models.MealRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("MealId") + .HasColumnType("int"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("MealId"); + + b.ToTable("MealRatings"); + }); + + modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CustomerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IngredientsJson") + .HasColumnType("longtext"); + + b.Property("OrderedAt") + .HasColumnType("datetime(6)"); + + b.Property("PizzaName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PizzaOrders"); + }); + + modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("P256DH") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PizzaOrderId") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PizzaOrderId"); + + b.HasIndex("UserId"); + + b.ToTable("PushSubscribers"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BaseMealId") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Inspiration") + .HasColumnType("longtext"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("Tags") + .HasColumnType("longtext"); + + b.Property("TestedBy") + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BaseMealId"); + + b.ToTable("RecipeLabEntries"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Instructions") + .HasColumnType("longtext"); + + b.Property("RecipeLabEntryId") + .HasColumnType("int"); + + b.Property("ResultNotes") + .HasColumnType("longtext"); + + b.Property("VersionLabel") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabEntryId"); + + b.ToTable("RecipeLabVersions"); + }); + + modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("P256DH") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PushSubscriptions"); + }); + + modelBuilder.Entity("Aberwyn.Models.TodoTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AssignedTo") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("TodoTasks"); + }); + + modelBuilder.Entity("Aberwyn.Models.UserPreferences", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("NotifyBudget") + .HasColumnType("tinyint(1)"); + + b.Property("NotifyMenu") + .HasColumnType("tinyint(1)"); + + b.Property("NotifyPizza") + .HasColumnType("tinyint(1)"); + + b.HasKey("UserId"); + + b.ToTable("UserPreferences"); + }); + + modelBuilder.Entity("Aberwyn.Models.WeeklyMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BreakfastMealId") + .HasColumnType("int"); + + b.Property("Cook") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DayOfWeek") + .HasColumnType("int"); + + b.Property("DinnerMealId") + .HasColumnType("int"); + + b.Property("LunchMealId") + .HasColumnType("int"); + + b.Property("WeekNumber") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("WeeklyMenu", (string)null); + }); + + modelBuilder.Entity("DownloadRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AutoDownload") + .HasColumnType("tinyint(1)"); + + b.Property("CategoryFilter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("KeywordFilter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxSize") + .HasColumnType("bigint"); + + b.Property("MinSeeders") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("DownloadRules"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("RssFeed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastChecked") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("RssFeeds"); + }); + + modelBuilder.Entity("TorrentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("longtext"); + + b.Property("Completed") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DownloadKey") + .HasColumnType("longtext"); + + b.Property("InfoHash") + .HasColumnType("varchar(255)"); + + b.Property("Leechers") + .HasColumnType("int"); + + b.Property("MagnetLink") + .HasColumnType("longtext"); + + b.Property("MovieName") + .HasColumnType("longtext"); + + b.Property("PublishDate") + .HasColumnType("datetime(6)"); + + b.Property("RssSource") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Seeders") + .HasColumnType("int"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Token") + .HasColumnType("longtext"); + + b.Property("TorrentUrl") + .HasColumnType("longtext"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InfoHash") + .IsUnique(); + + b.ToTable("TorrentItems"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition") + .WithMany() + .HasForeignKey("BudgetCategoryDefinitionId"); + + b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod") + .WithMany("Categories") + .HasForeignKey("BudgetPeriodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BudgetPeriod"); + + b.Navigation("Definition"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => + { + b.HasOne("Aberwyn.Models.BudgetCategory", null) + .WithMany("Items") + .HasForeignKey("BudgetCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition") + .WithMany() + .HasForeignKey("BudgetItemDefinitionId"); + + b.Navigation("BudgetItemDefinition"); + }); + + modelBuilder.Entity("Aberwyn.Models.Ingredient", b => + { + b.HasOne("Aberwyn.Models.Meal", null) + .WithMany("Ingredients") + .HasForeignKey("MealId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Aberwyn.Models.LabIngredient", b => + { + b.HasOne("Aberwyn.Models.RecipeLabEntry", "Entry") + .WithMany("Ingredients") + .HasForeignKey("RecipeLabEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabVersionIngredient", b => + { + b.HasOne("Aberwyn.Models.RecipeLabVersion", "Version") + .WithMany("Ingredients") + .HasForeignKey("RecipeLabVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Version"); + }); + + modelBuilder.Entity("Aberwyn.Models.Meal", b => + { + b.HasOne("Aberwyn.Models.MealCategory", "Category") + .WithMany("Meals") + .HasForeignKey("MealCategoryId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Aberwyn.Models.MealRating", b => + { + b.HasOne("Aberwyn.Models.Meal", "Meal") + .WithMany() + .HasForeignKey("MealId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Meal"); + }); + + modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b => + { + b.HasOne("Aberwyn.Models.PizzaOrder", "PizzaOrder") + .WithMany() + .HasForeignKey("PizzaOrderId"); + + b.HasOne("Aberwyn.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PizzaOrder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.HasOne("Aberwyn.Models.Meal", "BaseMeal") + .WithMany() + .HasForeignKey("BaseMealId"); + + b.Navigation("BaseMeal"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.HasOne("Aberwyn.Models.RecipeLabEntry", "Entry") + .WithMany("Versions") + .HasForeignKey("RecipeLabEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aberwyn.Models.UserPreferences", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", "User") + .WithOne("Preferences") + .HasForeignKey("Aberwyn.Models.UserPreferences", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b => + { + b.Navigation("Preferences") + .IsRequired(); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("Aberwyn.Models.Meal", b => + { + b.Navigation("Ingredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.MealCategory", b => + { + b.Navigation("Meals"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.Navigation("Ingredients"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.Navigation("Ingredients"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.cs b/Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.cs new file mode 100644 index 0000000..2c490c6 --- /dev/null +++ b/Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.cs @@ -0,0 +1,211 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aberwyn.Migrations +{ + public partial class MakeTorrentFieldsNullable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TorrentUrl", + table: "TorrentItems", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Token", + table: "TorrentItems", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "MovieName", + table: "TorrentItems", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "MagnetLink", + table: "TorrentItems", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "InfoHash", + table: "TorrentItems", + type: "varchar(255)", + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(255)") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "DownloadKey", + table: "TorrentItems", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Category", + table: "TorrentItems", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "TorrentItems", + keyColumn: "TorrentUrl", + keyValue: null, + column: "TorrentUrl", + value: ""); + + migrationBuilder.AlterColumn( + name: "TorrentUrl", + table: "TorrentItems", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "TorrentItems", + keyColumn: "Token", + keyValue: null, + column: "Token", + value: ""); + + migrationBuilder.AlterColumn( + name: "Token", + table: "TorrentItems", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "TorrentItems", + keyColumn: "MovieName", + keyValue: null, + column: "MovieName", + value: ""); + + migrationBuilder.AlterColumn( + name: "MovieName", + table: "TorrentItems", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "TorrentItems", + keyColumn: "MagnetLink", + keyValue: null, + column: "MagnetLink", + value: ""); + + migrationBuilder.AlterColumn( + name: "MagnetLink", + table: "TorrentItems", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "TorrentItems", + keyColumn: "InfoHash", + keyValue: null, + column: "InfoHash", + value: ""); + + migrationBuilder.AlterColumn( + name: "InfoHash", + table: "TorrentItems", + type: "varchar(255)", + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(255)", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "TorrentItems", + keyColumn: "DownloadKey", + keyValue: null, + column: "DownloadKey", + value: ""); + + migrationBuilder.AlterColumn( + name: "DownloadKey", + table: "TorrentItems", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "TorrentItems", + keyColumn: "Category", + keyValue: null, + column: "Category", + value: ""); + + migrationBuilder.AlterColumn( + name: "Category", + table: "TorrentItems", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs index 2c8a8fe..703b510 100644 --- a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs @@ -697,6 +697,34 @@ namespace Aberwyn.Migrations b.ToTable("WeeklyMenu", (string)null); }); + modelBuilder.Entity("DownloadRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AutoDownload") + .HasColumnType("tinyint(1)"); + + b.Property("CategoryFilter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("KeywordFilter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxSize") + .HasColumnType("bigint"); + + b.Property("MinSeeders") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("DownloadRules"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { b.Property("Id") @@ -825,6 +853,103 @@ namespace Aberwyn.Migrations b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("RssFeed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastChecked") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("RssFeeds"); + }); + + modelBuilder.Entity("TorrentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("longtext"); + + b.Property("Completed") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DownloadKey") + .HasColumnType("longtext"); + + b.Property("InfoHash") + .HasColumnType("varchar(255)"); + + b.Property("Leechers") + .HasColumnType("int"); + + b.Property("MagnetLink") + .HasColumnType("longtext"); + + b.Property("MovieName") + .HasColumnType("longtext"); + + b.Property("PublishDate") + .HasColumnType("datetime(6)"); + + b.Property("RssSource") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Seeders") + .HasColumnType("int"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Token") + .HasColumnType("longtext"); + + b.Property("TorrentUrl") + .HasColumnType("longtext"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InfoHash") + .IsUnique(); + + b.ToTable("TorrentItems"); + }); + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => { b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition") diff --git a/Aberwyn/Models/TorrentInfo.cs b/Aberwyn/Models/TorrentInfo.cs index 3856d60..290fff7 100644 --- a/Aberwyn/Models/TorrentInfo.cs +++ b/Aberwyn/Models/TorrentInfo.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + public class TorrentInfo { public string FileName { get; set; } @@ -26,4 +28,60 @@ public class TorrentUploadViewModel public IFormFile TorrentFile { get; set; } public TorrentInfo TorrentInfo { get; set; } public bool ShowResults { get; set; } = false; +} + +public class RssFeed +{ + public int Id { get; set; } + public string Url { get; set; } + public string Name { get; set; } + public DateTime LastChecked { get; set; } + public bool IsActive { get; set; } +} +public class TorrentItem +{ + public int Id { get; set; } + + [Required] + public string Title { get; set; } = string.Empty; + + public string? MagnetLink { get; set; } + public string? InfoHash { get; set; } + public DateTime PublishDate { get; set; } + public long Size { get; set; } = 0; + public int Seeders { get; set; } = 0; + public int Leechers { get; set; } = 0; + public int Completed { get; set; } = 0; + public TorrentStatus Status { get; set; } = TorrentStatus.New; + public string? Category { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Required] + public string RssSource { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + public string? TorrentUrl { get; set; } + public string? MovieName { get; set; } + public int? Year { get; set; } + public string? DownloadKey { get; set; } + public string? Token { get; set; } +} + +public enum TorrentStatus +{ + New, + Downloaded, + Processing, + Completed, + Failed, + Ignored +} +public class DownloadRule +{ + public int Id { get; set; } + public string KeywordFilter { get; set; } + public string CategoryFilter { get; set; } + public int MinSeeders { get; set; } + public long MaxSize { get; set; } + public bool AutoDownload { get; set; } } \ No newline at end of file diff --git a/Aberwyn/Program.cs b/Aberwyn/Program.cs index 55e657f..2ee5b86 100644 --- a/Aberwyn/Program.cs +++ b/Aberwyn/Program.cs @@ -66,6 +66,13 @@ catch (Exception ex) builder.Services.AddHttpClient(); builder.Services.AddScoped(); +builder.Services.AddHttpClient(); +builder.Services.AddScoped(); +builder.Services.AddHostedService(); + +builder.Services.AddHttpClient(); +builder.Services.AddScoped(); + // Add services to the container builder.Services.AddControllersWithViews() @@ -166,7 +173,12 @@ builder.Services.Configure(options => options.SupportedUICultures = supportedCultures; }); builder.Services.AddSingleton(); +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); +builder.Logging.AddDebug(); +// Eller om du vill ha mer detaljerad loggning: +builder.Logging.SetMinimumLevel(LogLevel.Information); var app = builder.Build(); app.UseStaticFiles(); diff --git a/Aberwyn/Views/Shared/_Layout.cshtml b/Aberwyn/Views/Shared/_Layout.cshtml index 82e7fd8..fa1acb6 100644 --- a/Aberwyn/Views/Shared/_Layout.cshtml +++ b/Aberwyn/Views/Shared/_Layout.cshtml @@ -47,6 +47,10 @@ @if (User.IsInRole("Budget")) {
  • Budget
  • + } + @if (User.IsInRole("Admin")) + { +
  • Torrents
  • } @if (User.IsInRole("Chef")) { diff --git a/Aberwyn/Views/Torrent/Index.cshtml b/Aberwyn/Views/Torrent/Index.cshtml index 288e2ad..4d72bea 100644 --- a/Aberwyn/Views/Torrent/Index.cshtml +++ b/Aberwyn/Views/Torrent/Index.cshtml @@ -1,225 +1,23 @@ -@model TorrentUploadViewModel -@{ - ViewData["Title"] = "Torrent Analyzer"; -} +@model IEnumerable + -
    -
    -
    -
    -
    -

    - Torrent Analyzer -

    -

    Ladda upp en torrent-fil för att se seeders/leechers

    -
    -
    - @if (!ViewData.ModelState.IsValid) - { -
    - @foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors)) - { -
    @error.ErrorMessage
    - } -
    - } - -
    - @Html.AntiForgeryToken() -
    - - -
    Endast .torrent filer, max 10MB
    -
    - -
    - - @if (Model.ShowResults && Model.TorrentInfo != null) - { -
    -
    -

    - Torrent Information -

    - -
    -
    Filnamn:
    -
    @Model.TorrentInfo.FileName
    -
    - -
    -
    Storlek:
    -
    @FormatFileSize(Model.TorrentInfo.Size)
    -
    - -
    -
    Announce URL:
    -
    - @Model.TorrentInfo.AnnounceUrl -
    -
    - -
    -
    Info Hash:
    -
    - @Model.TorrentInfo.InfoHash -
    -
    - - @if (!string.IsNullOrEmpty(Model.TorrentInfo.ErrorMessage)) - { -
    - - Tracker-info: @Model.TorrentInfo.ErrorMessage -
    - } - else if (Model.TorrentInfo.HasTrackerData) - { -
    -
    -
    - Tracker Statistik -
    - -
    -
    -
    -
    -
    -

    @Model.TorrentInfo.Seeders

    - Seeders -
    -
    -
    -
    -

    @Model.TorrentInfo.Leechers

    - Leechers -
    -
    -
    -
    -

    @Model.TorrentInfo.Completed

    - Completed -
    -
    -
    -
    -
    - } - else - { -
    - - Tracker-statistik kunde inte hämtas. Detta kan bero på att trackern inte stöder scraping eller kräver autentisering. -
    - } -
    - } -
    -
    -
    +
    +
    +
    Titel
    +
    Seeders
    +
    Leechers
    +
    Completed
    +
    Datum
    -
    -@functions { - string FormatFileSize(long bytes) + @foreach (var t in Model) { - if (bytes == 0) return "Okänd storlek"; - - string[] sizes = { "B", "KB", "MB", "GB", "TB" }; - int order = 0; - double size = bytes; - - while (size >= 1024 && order < sizes.Length - 1) - { - order++; - size = size / 1024; - } - - return $"{size:0.##} {sizes[order]}"; +
    +
    @t.Title
    +
    @t.Seeders
    +
    @t.Leechers
    +
    @t.Completed
    +
    @t.PublishDate.ToString("yyyy-MM-dd HH:mm")
    +
    } -} - - - - \ No newline at end of file +
    diff --git a/Aberwyn/wwwroot/css/torrent.css b/Aberwyn/wwwroot/css/torrent.css new file mode 100644 index 0000000..303b7ea --- /dev/null +++ b/Aberwyn/wwwroot/css/torrent.css @@ -0,0 +1,61 @@ +/* ========================================================== + TORRENT LIST STYLING + ========================================================== */ + +.torrent-list { + background-color: #fff; + border-radius: 8px; + padding: 10px 16px; + box-shadow: 0 2px 6px rgba(0,0,0,0.15); + margin: 16px 0; + font-size: 14px; + color: #1F2C3C; +} + +.torrent-header, .torrent-row { + display: grid; + grid-template-columns: 3fr 1fr 1fr 1fr 2fr; + align-items: center; + padding: 6px 0; +} + +.torrent-header { + font-weight: 600; + border-bottom: 2px solid #ccc; + margin-bottom: 4px; + color: #223344; +} + +.torrent-row { + border-bottom: 1px solid #e0e0e0; + transition: background-color 0.2s; +} + + .torrent-row:hover { + background-color: #f7f7f7; + } + +.col-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.col-center { + text-align: center; +} + +.col-right { + text-align: right; + font-size: 12px; + color: #666; +} + +.highlight-green { + color: #00cc66; + font-weight: bold; +} + +.highlight-red { + color: #cc3333; +}