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 MenuService _menuService;
private readonly ApplicationDbContext _context;
private readonly TorrentService _torrentService;
// 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;
_menuService = menuService;
_context = applicationDbContext;
_torrentService = torrentService;
}
public IActionResult Index()
public async Task<IActionResult> Index()
{
var isOpen = _context.AppSettings.FirstOrDefault(x => x.Key == "RestaurantIsOpen")?.Value == "True";
ViewBag.RestaurantIsOpen = isOpen;
@@ -32,11 +34,17 @@ namespace Aberwyn.Controllers
var showDate = now.Hour >= 20 ? now.Date.AddDays(1) : now.Date;
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;
return View(todaysMenu);
}
public IActionResult Privacy()
{
return View();

View File

@@ -1,5 +1,6 @@
using Aberwyn.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
public class TorrentController : Controller
{
@@ -7,26 +8,31 @@ public class TorrentController : Controller
private readonly ILogger<TorrentController> _logger;
private readonly DelugeClient _deluge;
private readonly MovieMetadataService _movieMetadataService;
private readonly ApplicationDbContext _context;
public TorrentController(
ITorrentService torrentService,
ILogger<TorrentController> logger,
DelugeClient delugeClient,
MovieMetadataService movieMetadataService)
MovieMetadataService movieMetadataService,
ApplicationDbContext context)
{
_torrentService = torrentService;
_logger = logger;
_deluge = delugeClient;
_movieMetadataService = movieMetadataService;
_context = context;
}
[HttpGet]
public async Task<IActionResult> Index(int page = 1, string sort = "date", string range = "all")
{
var pageSize = 20;
var userId = User.Identity?.Name ?? "guest";
var torrents = await _torrentService.GetRecentTorrentsAsync(500);
// filtrera på tidsintervall
// Filtrera på tidsintervall
torrents = range switch
{
"day" => torrents.Where(t => t.PublishDate > DateTime.UtcNow.AddDays(-1)).ToList(),
@@ -35,7 +41,7 @@ public class TorrentController : Controller
_ => torrents
};
// sortera
// Sortera
torrents = sort switch
{
"seeders" => torrents.OrderByDescending(t => t.Seeders).ToList(),
@@ -44,7 +50,42 @@ public class TorrentController : Controller
_ => 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
{
@@ -58,6 +99,8 @@ public class TorrentController : Controller
return View(vm);
}
[HttpPost]
public async Task<IActionResult> Add(string torrentUrl)
{

View File

@@ -64,6 +64,7 @@ namespace Aberwyn.Data
public DbSet<TorrentItem> TorrentItems { get; set; }
public DbSet<RssFeed> RssFeeds { 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;
}
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)
{

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");
});
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 =>
{
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")

View File

@@ -88,15 +88,25 @@ public class MovieMetadata
public class TorrentListViewModel
{
public List<TorrentItem> Items { get; set; } = new();
public List<TorrentListItemViewModel> Items { get; set; } = new();
public int CurrentPage { get; set; }
public int TotalPages { get; set; }
public string CurrentSort { get; set; } = "date";
public string CurrentRange { get; set; } = "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 Data data { get; set; }
@@ -161,3 +171,11 @@ public class DownloadRule
public long MaxSize { 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.AddHttpClient<DelugeClient>();
builder.Services.AddHttpClient<MovieMetadataService>();
builder.Services.AddScoped<ITorrentService, TorrentService>();
builder.Services.AddHttpClient<ITorrentService, TorrentService>();
builder.Services.AddScoped<TorrentService>();
// Add services to the container
builder.Services.AddControllersWithViews()

View File

@@ -50,7 +50,16 @@
}
@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"))
{
@@ -83,8 +92,6 @@
</ul>
</nav>
<main class="main-panel">
@RenderBody()
@RenderSection("Scripts", required: false)

View File

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

View File

@@ -465,3 +465,15 @@ body {
background-color: #e0f0ff;
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