Torrent v1 done
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-08-24 21:16:03 +02:00
parent 0c2f131fff
commit 85f559a607
15 changed files with 2790 additions and 48 deletions

View File

@@ -13,17 +13,19 @@ namespace Aberwyn.Controllers
//private readonly BudgetService _budgetService; //private readonly BudgetService _budgetService;
private readonly MenuService _menuService; private readonly MenuService _menuService;
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly TorrentService _torrentService;
// Constructor to inject dependencies // Constructor to inject dependencies
public HomeController(ApplicationDbContext applicationDbContext, ILogger<HomeController> logger, MenuService menuService) public HomeController(ApplicationDbContext applicationDbContext, ILogger<HomeController> logger, MenuService menuService, TorrentService torrentService)
{ {
_logger = logger; _logger = logger;
_menuService = menuService; _menuService = menuService;
_context = applicationDbContext; _context = applicationDbContext;
_torrentService = torrentService;
} }
public IActionResult Index() public async Task<IActionResult> Index()
{ {
var isOpen = _context.AppSettings.FirstOrDefault(x => x.Key == "RestaurantIsOpen")?.Value == "True"; var isOpen = _context.AppSettings.FirstOrDefault(x => x.Key == "RestaurantIsOpen")?.Value == "True";
ViewBag.RestaurantIsOpen = isOpen; ViewBag.RestaurantIsOpen = isOpen;
@@ -32,11 +34,17 @@ namespace Aberwyn.Controllers
var showDate = now.Hour >= 20 ? now.Date.AddDays(1) : now.Date; var showDate = now.Hour >= 20 ? now.Date.AddDays(1) : now.Date;
var todaysMenu = _menuService.GetMenuForDate(showDate); var todaysMenu = _menuService.GetMenuForDate(showDate);
var userId = User.Identity?.Name ?? "guest";
// Awaita async-metoden
var newCount = await _torrentService.GetUnseenTorrentCountAsync(userId);
ViewBag.NewTorrentCount = newCount;
ViewBag.ShowDate = showDate; ViewBag.ShowDate = showDate;
return View(todaysMenu); return View(todaysMenu);
} }
public IActionResult Privacy() public IActionResult Privacy()
{ {
return View(); return View();

View File

@@ -1,5 +1,6 @@
using Aberwyn.Data; using Aberwyn.Data;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
public class TorrentController : Controller public class TorrentController : Controller
{ {
@@ -7,26 +8,31 @@ public class TorrentController : Controller
private readonly ILogger<TorrentController> _logger; private readonly ILogger<TorrentController> _logger;
private readonly DelugeClient _deluge; private readonly DelugeClient _deluge;
private readonly MovieMetadataService _movieMetadataService; private readonly MovieMetadataService _movieMetadataService;
private readonly ApplicationDbContext _context;
public TorrentController( public TorrentController(
ITorrentService torrentService, ITorrentService torrentService,
ILogger<TorrentController> logger, ILogger<TorrentController> logger,
DelugeClient delugeClient, DelugeClient delugeClient,
MovieMetadataService movieMetadataService) MovieMetadataService movieMetadataService,
ApplicationDbContext context)
{ {
_torrentService = torrentService; _torrentService = torrentService;
_logger = logger; _logger = logger;
_deluge = delugeClient; _deluge = delugeClient;
_movieMetadataService = movieMetadataService; _movieMetadataService = movieMetadataService;
_context = context;
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> Index(int page = 1, string sort = "date", string range = "all") public async Task<IActionResult> Index(int page = 1, string sort = "date", string range = "all")
{ {
var pageSize = 20; var pageSize = 20;
var userId = User.Identity?.Name ?? "guest";
var torrents = await _torrentService.GetRecentTorrentsAsync(500); var torrents = await _torrentService.GetRecentTorrentsAsync(500);
// filtrera på tidsintervall // Filtrera på tidsintervall
torrents = range switch torrents = range switch
{ {
"day" => torrents.Where(t => t.PublishDate > DateTime.UtcNow.AddDays(-1)).ToList(), "day" => torrents.Where(t => t.PublishDate > DateTime.UtcNow.AddDays(-1)).ToList(),
@@ -35,7 +41,7 @@ public class TorrentController : Controller
_ => torrents _ => torrents
}; };
// sortera // Sortera
torrents = sort switch torrents = sort switch
{ {
"seeders" => torrents.OrderByDescending(t => t.Seeders).ToList(), "seeders" => torrents.OrderByDescending(t => t.Seeders).ToList(),
@@ -44,7 +50,42 @@ public class TorrentController : Controller
_ => torrents.OrderByDescending(t => t.PublishDate).ToList(), _ => torrents.OrderByDescending(t => t.PublishDate).ToList(),
}; };
var pagedItems = torrents.Skip((page - 1) * pageSize).Take(pageSize).ToList(); // Hämta sedda torrents för användaren
var seenHashes = await _context.UserTorrentSeen
.Where(x => x.UserId == userId)
.Select(x => x.InfoHash)
.ToListAsync();
// Bygg viewmodels med IsNew
var pagedItems = torrents
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(t => new TorrentListItemViewModel
{
InfoHash = t.InfoHash ?? "",
Title = t.Title,
MovieName = t.MovieName ?? "",
PublishDate = t.PublishDate,
Seeders = t.Seeders,
Leechers = t.Leechers,
TorrentUrl = t.TorrentUrl,
Metadata = t.Metadata,
IsNew = t.InfoHash != null && !seenHashes.Contains(t.InfoHash)
})
.ToList();
// Markera som sedda
var newSeen = pagedItems
.Where(i => i.IsNew && !string.IsNullOrEmpty(i.InfoHash))
.Select(i => new UserTorrentSeen
{
UserId = userId,
InfoHash = i.InfoHash,
SeenDate = DateTime.UtcNow
});
_context.UserTorrentSeen.AddRange(newSeen);
await _context.SaveChangesAsync();
var vm = new TorrentListViewModel var vm = new TorrentListViewModel
{ {
@@ -58,6 +99,8 @@ public class TorrentController : Controller
return View(vm); return View(vm);
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> Add(string torrentUrl) public async Task<IActionResult> Add(string torrentUrl)
{ {

View File

@@ -64,6 +64,7 @@ namespace Aberwyn.Data
public DbSet<TorrentItem> TorrentItems { get; set; } public DbSet<TorrentItem> TorrentItems { get; set; }
public DbSet<RssFeed> RssFeeds { get; set; } public DbSet<RssFeed> RssFeeds { get; set; }
public DbSet<DownloadRule> DownloadRules { get; set; } public DbSet<DownloadRule> DownloadRules { get; set; }
public DbSet<UserTorrentSeen> UserTorrentSeen { get; set; }
} }
} }

View File

@@ -167,6 +167,23 @@ public class TorrentService : ITorrentService
} }
return false; return false;
} }
public async Task<int> GetUnseenTorrentCountAsync(string userId)
{
// Hämta alla infohashes som användaren redan sett
var seenHashes = await _context.UserTorrentSeen
.Where(x => x.UserId == userId)
.Select(x => x.InfoHash)
.ToListAsync();
// Räkna alla torrents som inte finns i seenHashes och som har > 40 seeders
var count = await _context.TorrentItems
.Where(t => t.InfoHash != null
&& !seenHashes.Contains(t.InfoHash)
&& t.Seeders > 40)
.CountAsync();
return count;
}
private string ConvertAnnounceToScrape(string announceUrl) private string ConvertAnnounceToScrape(string announceUrl)
{ {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddUserTorrentSeen : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "UserTorrentSeen",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
TorrentId = table.Column<int>(type: "int", nullable: false),
SeenDate = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserTorrentSeen", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserTorrentSeen");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddUserTorrentSeenv2 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TorrentId",
table: "UserTorrentSeen");
migrationBuilder.AddColumn<string>(
name: "InfoHash",
table: "UserTorrentSeen",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "InfoHash",
table: "UserTorrentSeen");
migrationBuilder.AddColumn<int>(
name: "TorrentId",
table: "UserTorrentSeen",
type: "int",
nullable: false,
defaultValue: 0);
}
}
}

View File

@@ -950,6 +950,28 @@ namespace Aberwyn.Migrations
b.ToTable("TorrentItems"); b.ToTable("TorrentItems");
}); });
modelBuilder.Entity("UserTorrentSeen", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("InfoHash")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("SeenDate")
.HasColumnType("datetime(6)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("UserTorrentSeen");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{ {
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition") b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")

View File

@@ -88,15 +88,25 @@ public class MovieMetadata
public class TorrentListViewModel public class TorrentListViewModel
{ {
public List<TorrentItem> Items { get; set; } = new(); public List<TorrentListItemViewModel> Items { get; set; } = new();
public int CurrentPage { get; set; } public int CurrentPage { get; set; }
public int TotalPages { get; set; } public int TotalPages { get; set; }
public string CurrentSort { get; set; } = "date"; public string CurrentSort { get; set; } = "date";
public string CurrentRange { get; set; } = "all"; public string CurrentRange { get; set; } = "all";
public string CurrentPeriod { get; set; } = "all"; // day/week/month/all public string CurrentPeriod { get; set; } = "all"; // day/week/month/all
} }
public class TorrentListItemViewModel
{
public string InfoHash { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string MovieName { get; set; } = string.Empty;
public DateTime PublishDate { get; set; }
public int Seeders { get; set; }
public int Leechers { get; set; }
public string? TorrentUrl { get; set; }
public MovieMetadata? Metadata { get; set; }
public bool IsNew { get; set; } = false;
}
public class JustWatchResponse public class JustWatchResponse
{ {
public Data data { get; set; } public Data data { get; set; }
@@ -160,4 +170,12 @@ public class DownloadRule
public int MinSeeders { get; set; } public int MinSeeders { get; set; }
public long MaxSize { get; set; } public long MaxSize { get; set; }
public bool AutoDownload { get; set; } public bool AutoDownload { get; set; }
}
public class UserTorrentSeen
{
public int Id { get; set; }
public string UserId { get; set; } = null!;
public string InfoHash { get; set; } = null!; // unikt för torrent
public DateTime SeenDate { get; set; } = DateTime.UtcNow;
} }

View File

@@ -74,6 +74,9 @@ builder.Services.AddHttpClient<HdTorrentsTrackerScraper>();
builder.Services.AddScoped<HdTorrentsTrackerScraper>(); builder.Services.AddScoped<HdTorrentsTrackerScraper>();
builder.Services.AddHttpClient<DelugeClient>(); builder.Services.AddHttpClient<DelugeClient>();
builder.Services.AddHttpClient<MovieMetadataService>(); builder.Services.AddHttpClient<MovieMetadataService>();
builder.Services.AddScoped<ITorrentService, TorrentService>();
builder.Services.AddHttpClient<ITorrentService, TorrentService>();
builder.Services.AddScoped<TorrentService>();
// Add services to the container // Add services to the container
builder.Services.AddControllersWithViews() builder.Services.AddControllersWithViews()

View File

@@ -50,7 +50,16 @@
} }
@if (User.IsInRole("Admin")) @if (User.IsInRole("Admin"))
{ {
<li><a asp-controller="torrent" asp-action="Index"> Torrents</a></li> <li>
<a asp-controller="torrent" asp-action="Index"> Torrents
@if (ViewBag.NewTorrentCount > 0)
{
<span class="new-badge">@ViewBag.NewTorrentCount</span>
}
</a>
</li>
} }
@if (User.IsInRole("Chef")) @if (User.IsInRole("Chef"))
{ {
@@ -83,8 +92,6 @@
</ul> </ul>
</nav> </nav>
<main class="main-panel"> <main class="main-panel">
@RenderBody() @RenderBody()
@RenderSection("Scripts", required: false) @RenderSection("Scripts", required: false)

View File

@@ -9,7 +9,7 @@
<button class="@(Model.CurrentRange == "all" ? "active" : "")" onclick="location.href='?range=all'">All time</button> <button class="@(Model.CurrentRange == "all" ? "active" : "")" onclick="location.href='?range=all'">All time</button>
</div> </div>
<!-- Torrentlista med versioner --> <!-- Torrentlista -->
<div class="torrent-list"> <div class="torrent-list">
<div class="torrent-header"> <div class="torrent-header">
<div onclick="sortBy('title')" class="@(Model.CurrentSort == "title" ? "active" : "")">Titel</div> <div onclick="sortBy('title')" class="@(Model.CurrentSort == "title" ? "active" : "")">Titel</div>
@@ -23,32 +23,47 @@
.GroupBy(t => new { t.MovieName, t.Metadata?.Year }) .GroupBy(t => new { t.MovieName, t.Metadata?.Year })
.Select(g => new .Select(g => new
{ {
Title = g.Key.MovieName, MovieName = g.Key.MovieName,
Year = g.Key.Year, Year = g.Key.Year,
Versions = g Versions = g.OrderByDescending(t => t.Title.Contains("Fix") || t.Title.Contains("Repack"))
.OrderByDescending(t => t.Title.Contains("Fix") || t.Title.Contains("Repack"))
.ThenByDescending(t => t.Seeders) .ThenByDescending(t => t.Seeders)
.ToList() .ToList()
})) }))
{ {
var showBadge = group.Versions.Count > 1;
var main = group.Versions.First(); var main = group.Versions.First();
var lastVersion = group.Versions.Last();
<!-- Huvudrad --> <!-- Huvudrad -->
<div class="torrent-row torrent-group-title @(group.Versions.Count == 1 ? "last-row" : "")"> <div class="torrent-row torrent-group-title @(group.Versions.Count == 1 ? "last-row" : "")">
<div class="col-title"> <div class="col-title">
<a href="@main.Metadata?.Poster" class="glightbox"> @if (!string.IsNullOrEmpty(main.Metadata?.Poster) && main.Metadata.Poster != "N/A")
<img src="@main.Metadata?.Poster" alt="@main.Title" class="poster" /> {
</a> <a href="@main.Metadata.Poster" class="glightbox">
<div class="title-info"> <img src="@main.Metadata.Poster"
@if (!string.IsNullOrEmpty(main.Metadata?.Title)) alt="@main.MovieName"
{ class="poster"
<strong>@group.Title (@group.Year)</strong> onerror="this.onerror=null; this.src='/images/fallback.jpg';" />
} else </a>
{ }
<strong>@main.Title</strong> else
} {
<img src="/images/fallback.jpg" alt="@main.MovieName" class="poster placeholder" />
}
<div class="title-info">
@if (!string.IsNullOrEmpty(main.Metadata?.Title))
{
<strong>@group.MovieName (@group.Year) </strong>
} else
{
<strong>@main.Title </strong>
}
@if (main.IsNew)
{
<img src="/images/new.png" alt="New" class="badge" />
}
<div class="meta"> <div class="meta">
@if (!string.IsNullOrEmpty(main.Metadata?.Genre)) @if (!string.IsNullOrEmpty(main.Metadata?.Genre))
{ {
@@ -63,6 +78,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-date"> <div class="col-date">
<div class="time">@main.PublishDate.ToString("HH:mm")</div> <div class="time">@main.PublishDate.ToString("HH:mm")</div>
<div class="date">@main.PublishDate.ToString("yyyy-MM-dd")</div> <div class="date">@main.PublishDate.ToString("yyyy-MM-dd")</div>
@@ -80,34 +96,33 @@
<!-- Versioner --> <!-- Versioner -->
@if (group.Versions.Count > 1) @if (group.Versions.Count > 1)
{ {
var lastVersion = group.Versions.Last(); foreach (var version in group.Versions.Skip(1))
@foreach (var t in group.Versions.Skip(0))
{ {
var isLast = t == lastVersion; var isLast = version == lastVersion;
<div class="torrent-row torrent-version @(isLast ? "last-version" : "")" title="@t.Title"> <div class="torrent-row torrent-version @(isLast ? "last-version" : "")" title="@version.Title">
<div class="col-title"> <div class="col-title">
<strong>@t.Title</strong> <strong>
</div> @version.Title
<div> @if (version.IsNew)
@t.PublishDate.ToString("HH:mm yyyy-MM-dd") {
</div> <img src="/images/new.png" alt="New" class="badge" />
<div class="@(t.Seeders > 40 ? "highlight-green" : "")"> }
@t.Seeders </strong>
</div>
<div class="highlight-red">
@t.Leechers
</div> </div>
<div>@version.PublishDate.ToString("HH:mm yyyy-MM-dd")</div>
<div class="@(version.Seeders > 40 ? "highlight-green" : "")">@version.Seeders</div>
<div class="highlight-red">@version.Leechers</div>
<div class="col-action"> <div class="col-action">
<form asp-controller="Torrent" asp-action="Add" method="post" onsubmit="return confirmDownload('@t.Title')"> <form asp-controller="Torrent" asp-action="Add" method="post" onsubmit="return confirmDownload('@version.Title')">
<input type="hidden" name="torrentUrl" value="@t.TorrentUrl" /> <input type="hidden" name="torrentUrl" value="@version.TorrentUrl" />
<button type="submit" class="btn-add btn-small"></button> <button type="submit" class="btn-add btn-small"></button>
</form> </form>
</div> </div>
</div> </div>
} }
} }
} }
</div> </div>
<!-- Pagination --> <!-- Pagination -->
@@ -124,12 +139,11 @@
} }
} }
</div> </div>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/glightbox/dist/css/glightbox.min.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/glightbox/dist/css/glightbox.min.css" />
<script src="https://cdn.jsdelivr.net/npm/glightbox/dist/js/glightbox.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/glightbox/dist/js/glightbox.min.js"></script>
<script> <script>
const lightbox = GLightbox({ const lightbox = GLightbox({ selector: '.glightbox' });
selector: '.glightbox'
});
function sortBy(field){ function sortBy(field){
const url = new URL(window.location); const url = new URL(window.location);
url.searchParams.set('sort', field); url.searchParams.set('sort', field);

View File

@@ -465,3 +465,15 @@ body {
background-color: #e0f0ff; background-color: #e0f0ff;
color: #1F2C3C; color: #1F2C3C;
} }
.new-badge {
display: inline-block;
background-color: #ff3b30; /* röd likt Facebooks notis */
color: white;
font-size: 0.7rem;
font-weight: bold;
padding: 2px 6px;
border-radius: 12px;
margin-left: 0px;
vertical-align: middle;
line-height: 1;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B