From 6b19f08d6b5eb415f1be26169b52ca53539ccc0a Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Wed, 20 Aug 2025 00:39:19 +0200 Subject: [PATCH] More torrent and omdb --- Aberwyn/Controllers/TorrentController.cs | 26 +- Aberwyn/Data/ApplicationDbContext.cs | 3 + Aberwyn/Data/DelugeClient.cs | 70 + Aberwyn/Data/MovieMetadataService.cs | 50 + Aberwyn/Data/RssProcessor.cs | 12 +- ..._AddMovieMetadataToTorrentItem.Designer.cs | 1237 +++++++++++++++++ ...819221425_AddMovieMetadataToTorrentItem.cs | 125 ++ .../ApplicationDbContextModelSnapshot.cs | 49 + Aberwyn/Models/TorrentInfo.cs | 18 + Aberwyn/Program.cs | 7 +- Aberwyn/Views/Torrent/Index.cshtml | 32 +- Aberwyn/wwwroot/css/torrent.css | 83 +- 12 files changed, 1703 insertions(+), 9 deletions(-) create mode 100644 Aberwyn/Data/DelugeClient.cs create mode 100644 Aberwyn/Data/MovieMetadataService.cs create mode 100644 Aberwyn/Migrations/20250819221425_AddMovieMetadataToTorrentItem.Designer.cs create mode 100644 Aberwyn/Migrations/20250819221425_AddMovieMetadataToTorrentItem.cs diff --git a/Aberwyn/Controllers/TorrentController.cs b/Aberwyn/Controllers/TorrentController.cs index 8029b88..963393e 100644 --- a/Aberwyn/Controllers/TorrentController.cs +++ b/Aberwyn/Controllers/TorrentController.cs @@ -1,14 +1,20 @@ +using Aberwyn.Data; using Microsoft.AspNetCore.Mvc; public class TorrentController : Controller { private readonly ITorrentService _torrentService; private readonly ILogger _logger; + private readonly DelugeClient _deluge; + private readonly MovieMetadataService _movieMetadataService; - public TorrentController(ITorrentService torrentService, ILogger logger) + public TorrentController(ITorrentService torrentService, ILogger logger, DelugeClient delugeClient, MovieMetadataService movieMetadataService) { _torrentService = torrentService; _logger = logger; + _deluge = delugeClient; + _movieMetadataService = movieMetadataService; + } [HttpGet] @@ -18,6 +24,24 @@ public class TorrentController : Controller return View(torrents); // skicka lista av TorrentItem } + [HttpPost] + public async Task Add(string torrentUrl) + { + if (await _deluge.LoginAsync("deluge1")) + { + var success = await _deluge.AddTorrentUrlAsync(torrentUrl); + if (success) + { + TempData["Message"] = "Torrent tillagd i Deluge!"; + return RedirectToAction("Index"); + } + } + + TempData["Error"] = "Misslyckades att lägga till torrent."; + return RedirectToAction("Index"); + } + + [HttpPost] [ValidateAntiForgeryToken] public async Task Upload(TorrentUploadViewModel model) diff --git a/Aberwyn/Data/ApplicationDbContext.cs b/Aberwyn/Data/ApplicationDbContext.cs index edd3b9f..ba35993 100644 --- a/Aberwyn/Data/ApplicationDbContext.cs +++ b/Aberwyn/Data/ApplicationDbContext.cs @@ -1,6 +1,7 @@ using Aberwyn.Models; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using System.Reflection.Emit; namespace Aberwyn.Data { @@ -34,6 +35,8 @@ namespace Aberwyn.Data DisplayOrder = 1 } ); + builder.Entity() + .OwnsOne(t => t.Metadata); } diff --git a/Aberwyn/Data/DelugeClient.cs b/Aberwyn/Data/DelugeClient.cs new file mode 100644 index 0000000..a36e81b --- /dev/null +++ b/Aberwyn/Data/DelugeClient.cs @@ -0,0 +1,70 @@ +using MySqlX.XDevAPI; +using System.Text.Json; + +namespace Aberwyn.Data +{ + public class DelugeClient + { + private readonly HttpClient _http; + private readonly string _url; + private string _sessionId; + + public DelugeClient(HttpClient httpClient, string baseUrl = "http://192.168.1.3:8112/json") + { + _http = httpClient; + _url = baseUrl; + } + + public async Task LoginAsync(string password) + { + var payload = new + { + method = "auth.login", + @params = new object[] { password }, + id = 1 + }; + + var response = await _http.PostAsJsonAsync(_url, payload); + var json = await response.Content.ReadFromJsonAsync(); + + // spara sessioncookie för framtida requests + if (response.Headers.TryGetValues("Set-Cookie", out var cookies)) + { + _sessionId = cookies.FirstOrDefault()?.Split(';')[0]; + if (!_http.DefaultRequestHeaders.Contains("Cookie")) + _http.DefaultRequestHeaders.Add("Cookie", _sessionId); + } + + return json.GetProperty("result").GetBoolean(); + } + + public async Task AddMagnetAsync(string magnetLink) + { + var payload = new + { + method = "core.add_torrent_url", + @params = new object[] { magnetLink, new { } }, + id = 2 + }; + + var response = await _http.PostAsJsonAsync(_url, payload); + var json = await response.Content.ReadFromJsonAsync(); + return json.GetProperty("result").ValueKind != JsonValueKind.Null; + } + public async Task AddTorrentUrlAsync(string torrentUrl) + { + var payload = new + { + method = "core.add_torrent_url", + @params = new object[] { torrentUrl, new { } }, + id = 3 + }; + + var response = await _http.PostAsJsonAsync(_url, payload); + var json = await response.Content.ReadFromJsonAsync(); + + return json.GetProperty("result").ValueKind != JsonValueKind.Null; + } + + } +} diff --git a/Aberwyn/Data/MovieMetadataService.cs b/Aberwyn/Data/MovieMetadataService.cs new file mode 100644 index 0000000..eaf6188 --- /dev/null +++ b/Aberwyn/Data/MovieMetadataService.cs @@ -0,0 +1,50 @@ +namespace Aberwyn.Data +{ + public class MovieMetadataService + { + private readonly HttpClient _http; + private readonly string _apiKey = "6a666b45"; + + public MovieMetadataService(HttpClient httpClient) + { + _http = httpClient; + } + + public async Task GetMovieAsync(string title, int? year = null) + { + if (string.IsNullOrWhiteSpace(title)) + return null; + + try + { + MovieMetadata? metadata = null; + + // Först försök med titel + år + if (year.HasValue) + { + var urlWithYear = $"https://www.omdbapi.com/?t={Uri.EscapeDataString(title)}&y={year.Value}&apikey={_apiKey}"; + metadata = await _http.GetFromJsonAsync(urlWithYear); + } + + // Om inget hittas, försök bara med titel + if (metadata == null || string.IsNullOrEmpty(metadata.Title)) + { + var urlTitleOnly = $"https://www.omdbapi.com/?t={Uri.EscapeDataString(title)}&apikey={_apiKey}"; + metadata = await _http.GetFromJsonAsync(urlTitleOnly); + } + + // Returnera metadata om något hittades + return (metadata != null && !string.IsNullOrEmpty(metadata.Title)) ? metadata : null; + } + catch + { + return null; + } + } + + + } + + + +} diff --git a/Aberwyn/Data/RssProcessor.cs b/Aberwyn/Data/RssProcessor.cs index 6c65cf7..c0b987f 100644 --- a/Aberwyn/Data/RssProcessor.cs +++ b/Aberwyn/Data/RssProcessor.cs @@ -12,14 +12,16 @@ namespace Aberwyn.Data private readonly ILogger _logger; private readonly HttpClient _httpClient; private readonly HdTorrentsTrackerScraper _trackerScraper; + private readonly MovieMetadataService _movieMetadataService; public RssProcessor(ApplicationDbContext context, ILogger logger, - HttpClient httpClient, HdTorrentsTrackerScraper trackerScraper) + HttpClient httpClient, HdTorrentsTrackerScraper trackerScraper, MovieMetadataService movieMetadataService) { _context = context; _logger = logger; _httpClient = httpClient; _trackerScraper = trackerScraper; + _movieMetadataService = movieMetadataService; } public async Task ProcessRssFeeds() @@ -77,6 +79,13 @@ namespace Aberwyn.Data Console.WriteLine($"Scraped stats for {torrentItem.Title}: S:{seeders} L:{leechers} C:{completed}"); } + + var metadata = await _movieMetadataService.GetMovieAsync(torrentItem.MovieName, torrentItem.Year); + if (metadata != null) + { + torrentItem.Metadata = metadata; + } + _context.TorrentItems.Add(torrentItem); var savedChanges = await _context.SaveChangesAsync(); Console.WriteLine($"SaveChanges returned: {savedChanges}"); @@ -87,6 +96,7 @@ namespace Aberwyn.Data } } else { + var howLongAgo = DateTime.UtcNow.AddHours(-6); if (torrentItem.PublishDate >= howLongAgo) { diff --git a/Aberwyn/Migrations/20250819221425_AddMovieMetadataToTorrentItem.Designer.cs b/Aberwyn/Migrations/20250819221425_AddMovieMetadataToTorrentItem.Designer.cs new file mode 100644 index 0000000..b192081 --- /dev/null +++ b/Aberwyn/Migrations/20250819221425_AddMovieMetadataToTorrentItem.Designer.cs @@ -0,0 +1,1237 @@ +// +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("20250819221425_AddMovieMetadataToTorrentItem")] + partial class AddMovieMetadataToTorrentItem + { + 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("TorrentItem", b => + { + b.OwnsOne("MovieMetadata", "Metadata", b1 => + { + b1.Property("TorrentItemId") + .HasColumnType("int"); + + b1.Property("Actors") + .HasColumnType("longtext"); + + b1.Property("Director") + .HasColumnType("longtext"); + + b1.Property("Genre") + .HasColumnType("longtext"); + + b1.Property("ImdbID") + .HasColumnType("longtext"); + + b1.Property("ImdbRating") + .HasColumnType("longtext"); + + b1.Property("Plot") + .HasColumnType("longtext"); + + b1.Property("Poster") + .HasColumnType("longtext"); + + b1.Property("Runtime") + .HasColumnType("longtext"); + + b1.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b1.Property("Year") + .HasColumnType("longtext"); + + b1.HasKey("TorrentItemId"); + + b1.ToTable("TorrentItems"); + + b1.WithOwner() + .HasForeignKey("TorrentItemId"); + }); + + b.Navigation("Metadata"); + }); + + 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/20250819221425_AddMovieMetadataToTorrentItem.cs b/Aberwyn/Migrations/20250819221425_AddMovieMetadataToTorrentItem.cs new file mode 100644 index 0000000..7008891 --- /dev/null +++ b/Aberwyn/Migrations/20250819221425_AddMovieMetadataToTorrentItem.cs @@ -0,0 +1,125 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aberwyn.Migrations +{ + public partial class AddMovieMetadataToTorrentItem : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Metadata_Actors", + table: "TorrentItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Metadata_Director", + table: "TorrentItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Metadata_Genre", + table: "TorrentItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Metadata_ImdbID", + table: "TorrentItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Metadata_ImdbRating", + table: "TorrentItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Metadata_Plot", + table: "TorrentItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Metadata_Poster", + table: "TorrentItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Metadata_Runtime", + table: "TorrentItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Metadata_Title", + table: "TorrentItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Metadata_Year", + table: "TorrentItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Metadata_Actors", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "Metadata_Director", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "Metadata_Genre", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "Metadata_ImdbID", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "Metadata_ImdbRating", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "Metadata_Plot", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "Metadata_Poster", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "Metadata_Runtime", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "Metadata_Title", + table: "TorrentItems"); + + migrationBuilder.DropColumn( + name: "Metadata_Year", + table: "TorrentItems"); + } + } +} diff --git a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs index 703b510..c3152f7 100644 --- a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1143,6 +1143,55 @@ namespace Aberwyn.Migrations .IsRequired(); }); + modelBuilder.Entity("TorrentItem", b => + { + b.OwnsOne("MovieMetadata", "Metadata", b1 => + { + b1.Property("TorrentItemId") + .HasColumnType("int"); + + b1.Property("Actors") + .HasColumnType("longtext"); + + b1.Property("Director") + .HasColumnType("longtext"); + + b1.Property("Genre") + .HasColumnType("longtext"); + + b1.Property("ImdbID") + .HasColumnType("longtext"); + + b1.Property("ImdbRating") + .HasColumnType("longtext"); + + b1.Property("Plot") + .HasColumnType("longtext"); + + b1.Property("Poster") + .HasColumnType("longtext"); + + b1.Property("Runtime") + .HasColumnType("longtext"); + + b1.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b1.Property("Year") + .HasColumnType("longtext"); + + b1.HasKey("TorrentItemId"); + + b1.ToTable("TorrentItems"); + + b1.WithOwner() + .HasForeignKey("TorrentItemId"); + }); + + b.Navigation("Metadata"); + }); + modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b => { b.Navigation("Preferences") diff --git a/Aberwyn/Models/TorrentInfo.cs b/Aberwyn/Models/TorrentInfo.cs index 290fff7..dd45eae 100644 --- a/Aberwyn/Models/TorrentInfo.cs +++ b/Aberwyn/Models/TorrentInfo.cs @@ -13,6 +13,7 @@ public class TorrentInfo public int Completed { get; set; } = 0; public bool HasTrackerData { get; set; } = false; public string ErrorMessage { get; set; } + } public class TrackerInfo { @@ -65,6 +66,23 @@ public class TorrentItem public int? Year { get; set; } public string? DownloadKey { get; set; } public string? Token { get; set; } + + public MovieMetadata? Metadata { get; set; } + +} + +public class MovieMetadata +{ + public string Title { get; set; } = string.Empty; // required + public string? Year { get; set; } + public string? Poster { get; set; } + public string? ImdbRating { get; set; } + public string? Genre { get; set; } + public string? Plot { get; set; } + public string? Director { get; set; } + public string? Actors { get; set; } + public string? Runtime { get; set; } + public string? ImdbID { get; set; } } public enum TorrentStatus diff --git a/Aberwyn/Program.cs b/Aberwyn/Program.cs index 2ee5b86..dfec78a 100644 --- a/Aberwyn/Program.cs +++ b/Aberwyn/Program.cs @@ -59,7 +59,7 @@ try } catch (Exception ex) { - Console.WriteLine($"❌ Fel vid läsning av setup.json: {ex.Message}"); + Console.WriteLine($"Fel vid läsning av setup.json: {ex.Message}"); setup = new SetupSettings { IsConfigured = false }; } @@ -71,8 +71,9 @@ builder.Services.AddScoped(); builder.Services.AddHostedService(); builder.Services.AddHttpClient(); -builder.Services.AddScoped(); - +builder.Services.AddScoped(); +builder.Services.AddHttpClient(); +builder.Services.AddHttpClient(); // Add services to the container builder.Services.AddControllersWithViews() diff --git a/Aberwyn/Views/Torrent/Index.cshtml b/Aberwyn/Views/Torrent/Index.cshtml index 4d72bea..3628bad 100644 --- a/Aberwyn/Views/Torrent/Index.cshtml +++ b/Aberwyn/Views/Torrent/Index.cshtml @@ -8,16 +8,46 @@
Leechers
Completed
Datum
+
Åtgärd
@foreach (var t in Model) {
-
@t.Title
+
+ @t.Title +
+ @t.Title + @if (!string.IsNullOrEmpty(t.Metadata?.ImdbRating)) + { + ⭐ @t.Metadata.ImdbRating + } +
+
@t.Seeders
@t.Leechers
@t.Completed
@t.PublishDate.ToString("yyyy-MM-dd HH:mm")
+
+
+ + +
+
} + + +
+ +
+ + diff --git a/Aberwyn/wwwroot/css/torrent.css b/Aberwyn/wwwroot/css/torrent.css index 303b7ea..bd958bc 100644 --- a/Aberwyn/wwwroot/css/torrent.css +++ b/Aberwyn/wwwroot/css/torrent.css @@ -14,7 +14,7 @@ .torrent-header, .torrent-row { display: grid; - grid-template-columns: 3fr 1fr 1fr 1fr 2fr; + grid-template-columns: 3fr 1fr 1fr 1fr 2fr 1fr; align-items: center; padding: 6px 0; } @@ -36,9 +36,9 @@ } .col-title { + display: flex; + align-items: center; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } .col-center { @@ -50,7 +50,22 @@ font-size: 12px; color: #666; } +.title-info { + display: flex; + flex-direction: column; + overflow: hidden; +} +.imdb { + font-size: 0.9em; + color: rgba(0,0,0,0.15); + margin-top: 2px; +} + +/* Knapp alltid längst till höger */ +.col-action { + text-align: right; +} .highlight-green { color: #00cc66; font-weight: bold; @@ -59,3 +74,65 @@ .highlight-red { color: #cc3333; } +.torrent-row form { + margin: 0; +} + +.btn-add { + background-color: #3399ff; + border: none; + color: #fff; + font-size: 13px; + cursor: pointer; + transition: background-color 0.2s ease; +} + + .btn-add:hover { + background-color: #2389e0; + } + +.poster { + width: 40px; + height: 60px; + object-fit: cover; + cursor: pointer; + margin-right: 8px; + border-radius: 4px; +} + +.title-info { + display: inline-block; + vertical-align: top; +} + +.imdb { + font-size: 0.9em; + color: #ffcc00; + margin-left: 4px; +} + +/* Lightbox */ +#posterLightbox { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.8); + justify-content: center; + align-items: center; + z-index: 10000; +} + + #posterLightbox img { + max-width: 80%; + max-height: 80%; + border-radius: 8px; + } + +/* Justera knappstorlek */ +.btn-small { + padding: 4px 8px; + font-size: 0.85rem; +}