Compare commits

...

3 Commits

Author SHA1 Message Date
elias
fb24ffbf03 Merge branch 'master' of http://192.168.1.9:3000/Tai/Aberwyn
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-03 21:52:25 +02:00
elias
274f98baa4 Test 2025-08-03 21:47:02 +02:00
elias
80ffad6d86 Torrent 2025-08-03 21:46:06 +02:00
6 changed files with 515 additions and 0 deletions

View File

@@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="AngularJS.Core" Version="1.8.2" />
<PackageReference Include="BencodeNET" Version="5.0.0" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.67" />
<PackageReference Include="Lib.Net.Http.WebPush" Version="3.3.1" />

View File

@@ -0,0 +1,99 @@
using Microsoft.AspNetCore.Mvc;
public class TorrentController : Controller
{
private readonly ITorrentService _torrentService;
private readonly ILogger<TorrentController> _logger;
public TorrentController(ITorrentService torrentService, ILogger<TorrentController> logger)
{
_torrentService = torrentService;
_logger = logger;
}
[HttpGet]
public IActionResult Index()
{
return View(new TorrentUploadViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Upload(TorrentUploadViewModel model)
{
if (model.TorrentFile == null || model.TorrentFile.Length == 0)
{
ModelState.AddModelError("TorrentFile", "Vänligen välj en torrent-fil");
return View("Index", model);
}
if (!model.TorrentFile.FileName.EndsWith(".torrent", StringComparison.OrdinalIgnoreCase))
{
ModelState.AddModelError("TorrentFile", "Endast .torrent filer är tillåtna");
return View("Index", model);
}
if (model.TorrentFile.Length > 10 * 1024 * 1024) // 10MB limit
{
ModelState.AddModelError("TorrentFile", "Filen är för stor (max 10MB)");
return View("Index", model);
}
try
{
// Parsa torrent-filen
var torrentInfo = await _torrentService.ParseTorrentAsync(model.TorrentFile);
if (!string.IsNullOrEmpty(torrentInfo.ErrorMessage))
{
ModelState.AddModelError("", torrentInfo.ErrorMessage);
return View("Index", model);
}
// Försök hämta tracker-statistik
torrentInfo = await _torrentService.FetchTrackerStatsAsync(torrentInfo);
model.TorrentInfo = torrentInfo;
model.ShowResults = true;
return View("Index", model);
}
catch (Exception ex)
{
_logger.LogError(ex, "Fel vid uppladdning av torrent");
ModelState.AddModelError("", "Ett oväntat fel inträffade");
return View("Index", model);
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RefreshStats(string infoHash, string scrapeUrl)
{
try
{
var torrentInfo = new TorrentInfo
{
InfoHash = infoHash,
ScrapeUrl = scrapeUrl,
InfoHashBytes = Convert.FromHexString(infoHash.Replace("%", ""))
};
var updatedInfo = await _torrentService.FetchTrackerStatsAsync(torrentInfo);
return Json(new
{
success = updatedInfo.HasTrackerData,
seeders = updatedInfo.Seeders,
leechers = updatedInfo.Leechers,
completed = updatedInfo.Completed,
error = updatedInfo.ErrorMessage
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Fel vid uppdatering av tracker-stats");
return Json(new { success = false, error = "Fel vid uppdatering" });
}
}
}

View File

@@ -0,0 +1,159 @@
using BencodeNET.Parsing;
using BencodeNET.Torrents;
using BencodeNET.Objects;
using System.Text;
public interface ITorrentService
{
Task<TorrentInfo> ParseTorrentAsync(IFormFile file);
Task<TorrentInfo> FetchTrackerStatsAsync(TorrentInfo info);
}
public class TorrentService : ITorrentService
{
private readonly HttpClient _httpClient;
private readonly ILogger<TorrentService> _logger;
// Kända trackers och deras egenskaper
private readonly Dictionary<string, TrackerInfo> _knownTrackers = new()
{
["hdts-announce.ru"] = new TrackerInfo
{
Name = "HD-Torrents",
SupportsScraping = true, // Ändrat till true
RequiresAuth = false, // Kan fungera utan auth för scraping
IsPrivate = true,
Notes = "Privat tracker, scraping kan fungera utan inloggning"
}
};
public TorrentService(HttpClient httpClient, ILogger<TorrentService> logger)
{
_httpClient = httpClient;
_logger = logger;
_httpClient.Timeout = TimeSpan.FromSeconds(10);
}
public async Task<TorrentInfo> ParseTorrentAsync(IFormFile file)
{
try
{
using var stream = new MemoryStream();
await file.CopyToAsync(stream);
stream.Position = 0;
var parser = new TorrentParser();
var torrent = parser.Parse(stream);
var infoHash = torrent.GetInfoHashBytes();
var announceUrl = torrent.Trackers?.FirstOrDefault()?.FirstOrDefault()?.ToString();
return new TorrentInfo
{
FileName = torrent.DisplayName ?? file.FileName,
AnnounceUrl = announceUrl,
ScrapeUrl = ConvertAnnounceToScrape(announceUrl),
InfoHash = UrlEncodeInfoHash(infoHash),
InfoHashBytes = infoHash,
Size = torrent.TotalSize
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Fel vid parsing av torrent-fil");
return new TorrentInfo
{
FileName = file.FileName,
ErrorMessage = $"Kunde inte parsa torrent-filen: {ex.Message}"
};
}
}
public async Task<TorrentInfo> FetchTrackerStatsAsync(TorrentInfo info)
{
if (string.IsNullOrWhiteSpace(info.ScrapeUrl))
{
info.ErrorMessage = "Ingen scrape URL tillgänglig";
return info;
}
var url = $"{info.ScrapeUrl}?info_hash={info.InfoHash}";
_logger.LogInformation("Scraping tracker: {Url}", url);
try
{
var data = await _httpClient.GetByteArrayAsync(url);
var parser = new BencodeParser();
var bdict = parser.Parse<BDictionary>(data);
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))
{
info.HasTrackerData = true;
return info;
}
info.ErrorMessage = "Info hash hittades inte i tracker-svaret";
}
else
{
info.ErrorMessage = "Inget 'files' objekt i tracker-svaret";
}
}
catch (HttpRequestException ex)
{
info.ErrorMessage = $"HTTP fel: {ex.Message}";
_logger.LogWarning(ex, "HTTP fel vid tracker scraping");
}
catch (TaskCanceledException)
{
info.ErrorMessage = "Timeout vid anslutning till tracker";
_logger.LogWarning("Timeout vid tracker scraping");
}
catch (Exception ex)
{
info.ErrorMessage = $"Fel vid parsing: {ex.Message}";
_logger.LogError(ex, "Fel vid tracker scraping");
}
return info;
}
private bool TryGetStatsFromFiles(BDictionary files, string hashKey, TorrentInfo info)
{
if (files.TryGetValue(hashKey, out var hashEntry) && hashEntry is BDictionary stats)
{
info.Seeders = stats.TryGetInt("complete") ?? 0;
info.Leechers = stats.TryGetInt("incomplete") ?? 0;
info.Completed = stats.TryGetInt("downloaded") ?? 0;
return true;
}
return false;
}
private string ConvertAnnounceToScrape(string announceUrl)
{
if (string.IsNullOrEmpty(announceUrl))
return null;
return announceUrl.Replace("/announce", "/scrape");
}
private string UrlEncodeInfoHash(byte[] infoHash)
{
var sb = new StringBuilder();
foreach (byte b in infoHash)
{
sb.AppendFormat("%{0:x2}", b);
}
return sb.ToString();
}
}
public static class BDictionaryExtensions
{
public static int? TryGetInt(this BDictionary dict, string key)
{
return dict.TryGetValue(key, out var value) && value is BNumber num ? (int?)num.Value : null;
}
}

View File

@@ -0,0 +1,29 @@
public class TorrentInfo
{
public string FileName { get; set; }
public string AnnounceUrl { get; set; }
public string ScrapeUrl { get; set; }
public string InfoHash { get; set; }
public byte[] InfoHashBytes { get; set; }
public long Size { get; set; }
public int Seeders { get; set; } = 0;
public int Leechers { get; set; } = 0;
public int Completed { get; set; } = 0;
public bool HasTrackerData { get; set; } = false;
public string ErrorMessage { get; set; }
}
public class TrackerInfo
{
public string Name { get; set; }
public bool SupportsScraping { get; set; }
public bool RequiresAuth { get; set; }
public bool IsPrivate { get; set; }
public string Notes { get; set; }
}
public class TorrentUploadViewModel
{
public IFormFile TorrentFile { get; set; }
public TorrentInfo TorrentInfo { get; set; }
public bool ShowResults { get; set; } = false;
}

View File

@@ -64,6 +64,8 @@ catch (Exception ex)
}
builder.Services.AddHttpClient<ITorrentService, TorrentService>();
builder.Services.AddScoped<ITorrentService, TorrentService>();
// Add services to the container
builder.Services.AddControllersWithViews()

View File

@@ -0,0 +1,225 @@
@model TorrentUploadViewModel
@{
ViewData["Title"] = "Torrent Analyzer";
}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3 class="mb-0">
<i class="fas fa-download"></i> Torrent Analyzer
</h3>
<p class="text-muted mb-0">Ladda upp en torrent-fil för att se seeders/leechers</p>
</div>
<div class="card-body">
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger">
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
{
<div>@error.ErrorMessage</div>
}
</div>
}
<form asp-action="Upload" method="post" enctype="multipart/form-data" class="mb-4">
@Html.AntiForgeryToken()
<div class="mb-3">
<label for="torrentFile" class="form-label">Välj torrent-fil</label>
<input type="file"
class="form-control"
asp-for="TorrentFile"
accept=".torrent"
required>
<div class="form-text">Endast .torrent filer, max 10MB</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload"></i> Analysera Torrent
</button>
</form>
@if (Model.ShowResults && Model.TorrentInfo != null)
{
<hr>
<div class="torrent-results">
<h4 class="mb-3">
<i class="fas fa-info-circle"></i> Torrent Information
</h4>
<div class="row mb-3">
<div class="col-sm-3"><strong>Filnamn:</strong></div>
<div class="col-sm-9">@Model.TorrentInfo.FileName</div>
</div>
<div class="row mb-3">
<div class="col-sm-3"><strong>Storlek:</strong></div>
<div class="col-sm-9">@FormatFileSize(Model.TorrentInfo.Size)</div>
</div>
<div class="row mb-3">
<div class="col-sm-3"><strong>Announce URL:</strong></div>
<div class="col-sm-9">
<small class="text-muted font-monospace">@Model.TorrentInfo.AnnounceUrl</small>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3"><strong>Info Hash:</strong></div>
<div class="col-sm-9">
<small class="text-muted font-monospace">@Model.TorrentInfo.InfoHash</small>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.TorrentInfo.ErrorMessage))
{
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<strong>Tracker-info:</strong> @Model.TorrentInfo.ErrorMessage
</div>
}
else if (Model.TorrentInfo.HasTrackerData)
{
<div class="card bg-light">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-chart-bar"></i> Tracker Statistik
</h5>
<button type="button"
class="btn btn-sm btn-outline-primary"
onclick="refreshStats()"
id="refreshBtn">
<i class="fas fa-sync-alt"></i> Uppdatera
</button>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-4">
<div class="stat-box p-3">
<h3 class="text-success mb-1" id="seeders">@Model.TorrentInfo.Seeders</h3>
<small class="text-muted">Seeders</small>
</div>
</div>
<div class="col-md-4">
<div class="stat-box p-3">
<h3 class="text-warning mb-1" id="leechers">@Model.TorrentInfo.Leechers</h3>
<small class="text-muted">Leechers</small>
</div>
</div>
<div class="col-md-4">
<div class="stat-box p-3">
<h3 class="text-info mb-1" id="completed">@Model.TorrentInfo.Completed</h3>
<small class="text-muted">Completed</small>
</div>
</div>
</div>
</div>
</div>
}
else
{
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
Tracker-statistik kunde inte hämtas. Detta kan bero på att trackern inte stöder scraping eller kräver autentisering.
</div>
}
</div>
}
</div>
</div>
</div>
</div>
</div>
@functions {
string FormatFileSize(long bytes)
{
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]}";
}
}
<style>
.stat-box {
border: 1px solid #dee2e6;
border-radius: 8px;
background: white;
margin: 5px;
}
.torrent-results {
animation: fadeIn 0.5s ease-in;
}
@@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.font-monospace {
font-family: 'Courier New', monospace;
word-break: break-all;
}
</style>
<script>
async function refreshStats() {
const refreshBtn = document.getElementById('refreshBtn');
const seedersEl = document.getElementById('seeders');
const leechersEl = document.getElementById('leechers');
const completedEl = document.getElementById('completed');
if (!seedersEl) return; // Ingen tracker data att uppdatera
refreshBtn.disabled = true;
refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Uppdaterar...';
try {
const response = await fetch('@Url.Action("RefreshStats", "Torrent")', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
},
body: JSON.stringify({
infoHash: '@Model.TorrentInfo?.InfoHash',
scrapeUrl: '@Model.TorrentInfo?.ScrapeUrl'
})
});
const data = await response.json();
if (data.success) {
seedersEl.textContent = data.seeders;
leechersEl.textContent = data.leechers;
completedEl.textContent = data.completed;
// Visa success animation
[seedersEl, leechersEl, completedEl].forEach(el => {
el.style.transform = 'scale(1.1)';
setTimeout(() => el.style.transform = 'scale(1)', 200);
});
} else {
alert('Fel vid uppdatering: ' + (data.error || 'Okänt fel'));
}
} catch (error) {
console.error('Fel:', error);
alert('Nätverksfel vid uppdatering');
} finally {
refreshBtn.disabled = false;
refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i> Uppdatera';
}
}
</script>