Torrent changes again!
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-08-24 01:20:46 +02:00
parent e96696f6be
commit 0c2f131fff
4 changed files with 295 additions and 156 deletions

View File

@@ -8,20 +8,54 @@ public class TorrentController : Controller
private readonly DelugeClient _deluge; private readonly DelugeClient _deluge;
private readonly MovieMetadataService _movieMetadataService; private readonly MovieMetadataService _movieMetadataService;
public TorrentController(ITorrentService torrentService, ILogger<TorrentController> logger, DelugeClient delugeClient, MovieMetadataService movieMetadataService) public TorrentController(
ITorrentService torrentService,
ILogger<TorrentController> logger,
DelugeClient delugeClient,
MovieMetadataService movieMetadataService)
{ {
_torrentService = torrentService; _torrentService = torrentService;
_logger = logger; _logger = logger;
_deluge = delugeClient; _deluge = delugeClient;
_movieMetadataService = movieMetadataService; _movieMetadataService = movieMetadataService;
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> Index() public async Task<IActionResult> Index(int page = 1, string sort = "date", string range = "all")
{ {
var torrents = await _torrentService.GetRecentTorrentsAsync(50); var pageSize = 20;
return View(torrents); // skicka lista av TorrentItem var torrents = await _torrentService.GetRecentTorrentsAsync(500);
// filtrera på tidsintervall
torrents = range switch
{
"day" => torrents.Where(t => t.PublishDate > DateTime.UtcNow.AddDays(-1)).ToList(),
"week" => torrents.Where(t => t.PublishDate > DateTime.UtcNow.AddDays(-7)).ToList(),
"month" => torrents.Where(t => t.PublishDate > DateTime.UtcNow.AddMonths(-1)).ToList(),
_ => torrents
};
// sortera
torrents = sort switch
{
"seeders" => torrents.OrderByDescending(t => t.Seeders).ToList(),
"leechers" => torrents.OrderByDescending(t => t.Leechers).ToList(),
"title" => torrents.OrderBy(t => t.MovieName).ToList(),
_ => torrents.OrderByDescending(t => t.PublishDate).ToList(),
};
var pagedItems = torrents.Skip((page - 1) * pageSize).Take(pageSize).ToList();
var vm = new TorrentListViewModel
{
Items = pagedItems,
CurrentPage = page,
TotalPages = (int)Math.Ceiling(torrents.Count / (double)pageSize),
CurrentSort = sort,
CurrentRange = range
};
return View(vm);
} }
[HttpPost] [HttpPost]
@@ -41,7 +75,6 @@ public class TorrentController : Controller
return RedirectToAction("Index"); return RedirectToAction("Index");
} }
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Upload(TorrentUploadViewModel model) public async Task<IActionResult> Upload(TorrentUploadViewModel model)
@@ -66,16 +99,14 @@ public class TorrentController : Controller
try try
{ {
// Parsa torrent-filen
var torrentInfo = await _torrentService.ParseTorrentAsync(model.TorrentFile); var torrentInfo = await _torrentService.ParseTorrentAsync(model.TorrentFile);
if (!string.IsNullOrEmpty(torrentInfo.ErrorMessage)) if (!string.IsNullOrEmpty(torrentInfo.ErrorMessage))
{ {
ModelState.AddModelError("", torrentInfo.ErrorMessage); ModelState.AddModelError("", torrentInfo.ErrorMessage);
return View("Index", model); return View("Index", model);
} }
// Försök hämta tracker-statistik
torrentInfo = await _torrentService.FetchTrackerStatsAsync(torrentInfo); torrentInfo = await _torrentService.FetchTrackerStatsAsync(torrentInfo);
model.TorrentInfo = torrentInfo; model.TorrentInfo = torrentInfo;
@@ -105,7 +136,7 @@ public class TorrentController : Controller
}; };
var updatedInfo = await _torrentService.FetchTrackerStatsAsync(torrentInfo); var updatedInfo = await _torrentService.FetchTrackerStatsAsync(torrentInfo);
return Json(new return Json(new
{ {
success = updatedInfo.HasTrackerData, success = updatedInfo.HasTrackerData,
@@ -121,19 +152,4 @@ public class TorrentController : Controller
return Json(new { success = false, error = "Fel vid uppdatering" }); return Json(new { success = false, error = "Fel vid uppdatering" });
} }
} }
[HttpGet] }
public async Task<IActionResult> 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<TorrentItem>());
}
}
}

View File

@@ -85,6 +85,18 @@ public class MovieMetadata
public string? ImdbID { get; set; } public string? ImdbID { get; set; }
public string? Providers { get; set; } public string? Providers { get; set; }
} }
public class TorrentListViewModel
{
public List<TorrentItem> 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 JustWatchResponse public class JustWatchResponse
{ {
public Data data { get; set; } public Data data { get; set; }

View File

@@ -1,64 +1,141 @@
@model IEnumerable<TorrentItem> @model TorrentListViewModel
<link rel="stylesheet" href="~/css/torrent.css" /> <link rel="stylesheet" href="~/css/torrent.css" />
<!-- Periodval -->
<div class="torrent-period">
<button class="@(Model.CurrentRange == "day" ? "active" : "")" onclick="location.href='?range=day'">Dag</button>
<button class="@(Model.CurrentRange == "week" ? "active" : "")" onclick="location.href='?range=week'">Vecka</button>
<button class="@(Model.CurrentRange == "month" ? "active" : "")" onclick="location.href='?range=month'">Månad</button>
<button class="@(Model.CurrentRange == "all" ? "active" : "")" onclick="location.href='?range=all'">All time</button>
</div>
<!-- Torrentlista med versioner -->
<div class="torrent-list"> <div class="torrent-list">
<div class="torrent-header"> <div class="torrent-header">
<div class="col-title">Film</div> <div onclick="sortBy('title')" class="@(Model.CurrentSort == "title" ? "active" : "")">Titel</div>
<div class="col-center">Seeders</div> <div onclick="sortBy('date')" class="@(Model.CurrentSort == "date" ? "active" : "")">Tid / Datum</div>
<div class="col-center">Leechers</div> <div onclick="sortBy('seeders')" class="@(Model.CurrentSort == "seeders" ? "active" : "")">Seeders</div>
<div class="col-right">Datum</div> <div onclick="sortBy('leechers')" class="@(Model.CurrentSort == "leechers" ? "active" : "")">Leechers</div>
<div class="col-right">Åtgärd</div> <div>Åtgärd</div>
</div> </div>
@foreach (var t in Model) @foreach (var group in Model.Items
.GroupBy(t => new { t.MovieName, t.Metadata?.Year })
.Select(g => new
{
Title = g.Key.MovieName,
Year = g.Key.Year,
Versions = g
.OrderByDescending(t => t.Title.Contains("Fix") || t.Title.Contains("Repack"))
.ThenByDescending(t => t.Seeders)
.ToList()
}))
{ {
<div class="torrent-row"> var showBadge = group.Versions.Count > 1;
var main = group.Versions.First();
<!-- Huvudrad -->
<div class="torrent-row torrent-group-title @(group.Versions.Count == 1 ? "last-row" : "")">
<div class="col-title"> <div class="col-title">
<img src="@t.Metadata?.Poster" alt="@t.Title" class="poster" onclick="showLightbox(this.src)" /> <a href="@main.Metadata?.Poster" class="glightbox">
<img src="@main.Metadata?.Poster" alt="@main.Title" class="poster" />
</a>
<div class="title-info"> <div class="title-info">
@if (!string.IsNullOrEmpty(t.Metadata?.Title)) @if (!string.IsNullOrEmpty(main.Metadata?.Title))
{ {
<strong>@t.MovieName (@t.Metadata?.Year)</strong> <strong>@group.Title (@group.Year)</strong>
} else } else
{ {
<strong>@t.Title</strong> <strong>@main.Title</strong>
} }
<div class="meta"> <div class="meta">
@if (!string.IsNullOrEmpty(t.Metadata?.Genre)) @if (!string.IsNullOrEmpty(main.Metadata?.Genre))
{ {
<span class="genre">@t.Metadata.Genre</span> <span class="genre">@main.Metadata.Genre</span>
} }
@if (!string.IsNullOrEmpty(t.Metadata?.ImdbRating)) @if (!string.IsNullOrEmpty(main.Metadata?.ImdbID))
{ {
<span class="imdb">⭐ @t.Metadata.ImdbRating</span> <a class="imdb" href="https://www.imdb.com/title/@main.Metadata.ImdbID" target="_blank">
⭐ @main.Metadata.ImdbRating
</a>
} }
</div> </div>
</div> </div>
</div> </div>
<div class="col-center @(t.Seeders > 40 ? "highlight-green" : "")">@t.Seeders</div> <div class="col-date">
<div class="col-center highlight-red">@t.Leechers</div> <div class="time">@main.PublishDate.ToString("HH:mm")</div>
<div class="col-right">@t.PublishDate.ToString("yyyy-MM-dd HH:mm")</div> <div class="date">@main.PublishDate.ToString("yyyy-MM-dd")</div>
<div class="col-right col-action"> </div>
<form asp-controller="Torrent" asp-action="Add" method="post"> <div class="col-center @(main.Seeders > 40 ? "highlight-green" : "")">@main.Seeders</div>
<input type="hidden" name="torrentUrl" value="@t.TorrentUrl" /> <div class="col-center highlight-red">@main.Leechers</div>
<button type="submit" class="btn-add btn-small"> Lägg till</button> <div class="col-action">
<form asp-controller="Torrent" asp-action="Add" method="post" onsubmit="return confirmDownload('@main.Title')">
<input type="hidden" name="torrentUrl" value="@main.TorrentUrl" />
<button type="submit" class="btn-add btn-small"> Ladda ner</button>
</form> </form>
</div> </div>
</div> </div>
<!-- Versioner -->
@if (group.Versions.Count > 1)
{
var lastVersion = group.Versions.Last();
@foreach (var t in group.Versions.Skip(0))
{
var isLast = t == lastVersion;
<div class="torrent-row torrent-version @(isLast ? "last-version" : "")" title="@t.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
</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" />
<button type="submit" class="btn-add btn-small"></button>
</form>
</div>
</div>
}
}
} }
</div> </div>
<!-- Lightbox för poster --> <!-- Pagination -->
<div id="posterLightbox" onclick="this.style.display='none'"> <div class="pagination">
<img id="lightboxImg" /> @for (int i = 1; i <= Model.TotalPages; i++)
{
if (i == Model.CurrentPage)
{
<span class="current">@i</span>
}
else
{
<a asp-route-page="@i" asp-route-sort="@Model.CurrentSort" asp-route-range="@Model.CurrentRange">@i</a>
}
}
</div> </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> <script>
function showLightbox(src) { const lightbox = GLightbox({
const lb = document.getElementById('posterLightbox'); selector: '.glightbox'
const img = document.getElementById('lightboxImg'); });
img.src = src; function sortBy(field){
lb.style.display = 'flex'; const url = new URL(window.location);
url.searchParams.set('sort', field);
window.location = url;
}
function confirmDownload(title) {
return confirm(`Vill du ladda ner "${title}"?`);
} }
</script> </script>

View File

@@ -1,70 +1,111 @@
/* ========================================================== .torrent-list {
TORRENT LIST STYLING max-width: 900px;
========================================================== */ margin: 16px auto;
.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; font-size: 14px;
color: #1F2C3C; color: #1F2C3C;
border-collapse: collapse;
} }
.torrent-header, .torrent-row { .torrent-header, .torrent-row {
display: grid; display: grid;
grid-template-columns: 4fr 1fr 1fr 2fr 1fr; grid-template-columns: 4fr 2fr 1fr 1fr 1fr;
align-items: center; align-items: center;
padding: 4px 0; border-bottom: 1px solid #ccc;
padding: 6px 0;
} }
.torrent-header { .torrent-header {
font-weight: 600; font-weight: 600;
border-bottom: 2px solid #ccc; border-bottom: 2px solid #333;
margin-bottom: 4px;
color: #223344; color: #223344;
} }
.torrent-row {
border-bottom: 1px solid #e0e0e0;
transition: background-color 0.2s;
}
.torrent-row:hover {
background-color: #f7f7f7;
}
.col-title { .col-title {
display: flex; display: flex;
align-items: center; align-items: center;
overflow: hidden; }
.col-date {
display: flex;
flex-direction: column;
text-align: center;
} }
.col-center { .col-center {
text-align: center; text-align: center;
} }
.col-right {
text-align: right;
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 { .col-action {
text-align: right; text-align: right;
} }
.poster {
width: 28px;
height: 40px;
margin-right: 10px;
border-radius: 4px;
}
/* Versioner kompakt */
.torrent-version {
display: flex;
align-items: center;
gap: 16px; /* Mellanslag mellan titel, datum, seeders, leechers och knapp */
padding: 2px 36px;
font-size: 11px;
border-bottom: none;
}
.torrent-version .col-title {
flex: 1;
overflow: hidden;
white-space: wrap;
text-overflow: ellipsis;
}
.torrent-version .col-date {
flex: 1;
display: flex;
flex-direction: column;
font-size: 12px;
text-align: center;
}
.torrent-version .col-center {
flex: 0.6;
text-align: center;
font-size: 12px;
}
.torrent-version .col-action {
flex: 1;
text-align: right;
}
.torrent-version:hover {
background-color: rgba(240, 240, 240, 0.3);
}
/* Huvudrad fortfarande grid */
.torrent-row.torrent-group-title {
display: grid;
grid-template-columns: 4fr 2fr 1fr 1fr 1fr;
align-items: center;
border-bottom: none;
padding: 6px 0;
}
.torrent-version.last-version {
border-bottom: 1px solid #ccc; /* Streck under sista versionen */
}
.torrent-row:hover {
background-color: #f7f7f7;
}
.torrent-row.last-row {
border-bottom: 1px solid #ccc; /* streck under sista raden i gruppen */
}
/* Highlight */
.highlight-green { .highlight-green {
color: #00cc66; color: #00cc66;
font-weight: bold; font-weight: bold;
@@ -73,82 +114,75 @@
.highlight-red { .highlight-red {
color: #cc3333; color: #cc3333;
} }
.torrent-row form {
margin: 0;
}
/* Buttons */
.btn-add { .btn-add {
background-color: #3399ff; background-color: #3399ff;
border: none;
color: #fff; color: #fff;
font-size: 13px; border: none;
border-radius: 4px;
padding: 3px 8px;
font-size: 12px;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease;
} }
.btn-add:hover { .btn-add:hover {
background-color: #2389e0; background-color: #2389e0;
} }
.poster { /* Periodval */
width: 25px; .torrent-period {
height: 35px;
object-fit: cover;
cursor: pointer;
margin-right: 10px;
border-radius: 4px;
}
.title-info {
display: flex; display: flex;
flex-direction: column; gap: 8px;
overflow: hidden; margin-bottom: 12px;
} }
.title-info strong { .torrent-period button {
font-size: 14px; background: #eee;
margin-bottom: 2px; border: none;
padding: 4px 12px;
border-radius: 12px;
cursor: pointer;
} }
.meta { .torrent-period button.active {
font-size: 12px; background: #3399ff;
color: #666; color: #fff;
font-weight: bold;
}
/* Pagination */
.pagination {
text-align: center;
margin-top: 16px;
} }
.genre { .pagination a, .pagination span {
margin-right: 8px; display: inline-block;
color: #888; margin: 0 4px;
} padding: 4px 8px;
border-radius: 4px;
background: #eee;
color: #333;
text-decoration: none;
}
.pagination .current {
background: #3399ff;
color: #fff;
font-weight: bold;
}
.pagination a:hover {
background: #ddd;
}
.imdb { .imdb {
font-size: 12px; font-size: 12px;
color: #5a4800; color: #5a4800;
text-decoration: none;
} }
.imdb:hover {
/* Lightbox */ text-decoration: underline;
#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;
}