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

This commit is contained in:
Elias Jansson
2025-08-19 15:56:52 +02:00
parent fb24ffbf03
commit 146c557c25
19 changed files with 4736 additions and 235 deletions

View File

@@ -12,9 +12,10 @@ public class TorrentController : Controller
}
[HttpGet]
public IActionResult Index()
public async Task<IActionResult> Index()
{
return View(new TorrentUploadViewModel());
var torrents = await _torrentService.GetRecentTorrentsAsync(50);
return View(torrents); // skicka lista av TorrentItem
}
[HttpPost]
@@ -96,4 +97,19 @@ public class TorrentController : Controller
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

@@ -14,7 +14,13 @@ namespace Aberwyn.Data
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<TorrentItem>()
.HasIndex(t => t.InfoHash)
.IsUnique();
builder.Entity<TorrentItem>()
.Property(t => t.Status)
.HasConversion<string>();
builder.Entity<WeeklyMenu>().ToTable("WeeklyMenu");
builder.Entity<MealCategory>().HasData(
new MealCategory
@@ -52,7 +58,9 @@ namespace Aberwyn.Data
public DbSet<LabIngredient> LabIngredients { get; set; }
public DbSet<LabVersionIngredient> LabVersionIngredients { get; set; }
public DbSet<MealRating> MealRatings { get; set; }
public DbSet<TorrentItem> TorrentItems { get; set; }
public DbSet<RssFeed> RssFeeds { get; set; }
public DbSet<DownloadRule> DownloadRules { get; set; }
}
}

View File

@@ -0,0 +1,103 @@
using System.Text;
namespace Aberwyn.Data
{
public class HdTorrentsTrackerScraper
{
private readonly HttpClient _httpClient;
private readonly ILogger<HdTorrentsTrackerScraper> _logger;
public HdTorrentsTrackerScraper(HttpClient httpClient, ILogger<HdTorrentsTrackerScraper> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<(int seeders, int leechers, int completed)> ScrapeHdTorrents(string infoHash, string downloadKey)
{
try
{
if (string.IsNullOrEmpty(infoHash) || string.IsNullOrEmpty(downloadKey))
return (0, 0, 0);
// Konvertera hex InfoHash till URL-encoded bytes
var infoHashBytes = HexStringToBytes(infoHash);
//%67%2e%d6%f9%30%e9%33%88%e3%1b%c6%f0%85%f5%f7%78%44%05%10%e7
var encodedInfoHash = Uri.EscapeDataString(Encoding.GetEncoding("ISO-8859-1").GetString(infoHashBytes));
var encodedInfoHashv2 = EncodeInfoHash(infoHash);
// Bygga scrape URL baserat på HD-Torrents format
var scrapeUrl = $"https://hdts-announce.ru/scrape.php?pid=98d498dedff78ba0334f662d151eb19b7?info_hash={encodedInfoHashv2}";
_logger.LogInformation("Scraping HD-Torrents: {Url}", scrapeUrl);
var response = await _httpClient.GetAsync(scrapeUrl);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsByteArrayAsync(); // Fix: Content.ReadAsByteArrayAsync()
return ParseBencodeResponse(content);
}
else
{
_logger.LogWarning("Scrape failed with status: {StatusCode}", response.StatusCode);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to scrape HD-Torrents for InfoHash: {InfoHash}", infoHash);
}
return (0, 0, 0);
}
private static string EncodeInfoHash(string hex)
{
var bytes = HexStringToBytes(hex);
return string.Concat(bytes.Select(b => $"%{b:X2}")).ToLower();
}
private static byte[] HexStringToBytes(string hex)
{
if (hex.Length % 2 != 0)
throw new ArgumentException("Invalid hex string length.");
var bytes = new byte[hex.Length / 2];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return bytes;
}
private (int seeders, int leechers, int completed) ParseBencodeResponse(byte[] data)
{
try
{
// Enkel bencode parsing för tracker response
var response = System.Text.Encoding.UTF8.GetString(data);
// Tracker response är vanligtvis i format:
// d5:filesd20:[info_hash]d8:completei[antal]e9:downloadedi[antal]e10:incompletei[antal]eee
// Hitta complete (seeders)
var completeMatch = System.Text.RegularExpressions.Regex.Match(response, @"8:completei(\d+)e");
var seeders = completeMatch.Success ? int.Parse(completeMatch.Groups[1].Value) : 0;
// Hitta incomplete (leechers)
var incompleteMatch = System.Text.RegularExpressions.Regex.Match(response, @"10:incompletei(\d+)e");
var leechers = incompleteMatch.Success ? int.Parse(incompleteMatch.Groups[1].Value) : 0;
// Hitta downloaded (completed)
var downloadedMatch = System.Text.RegularExpressions.Regex.Match(response, @"9:downloadedi(\d+)e");
var completed = downloadedMatch.Success ? int.Parse(downloadedMatch.Groups[1].Value) : 0;
return (seeders, leechers, completed);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to parse bencode response");
return (0, 0, 0);
}
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Aberwyn.Data
{
public interface IRssProcessor
{
Task ProcessRssFeeds();
}
}

View File

@@ -0,0 +1,286 @@
using Aberwyn.Data;
using Aberwyn.Models;
using Microsoft.EntityFrameworkCore;
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace Aberwyn.Data
{
public class RssProcessor : IRssProcessor
{
private readonly ApplicationDbContext _context;
private readonly ILogger<RssProcessor> _logger;
private readonly HttpClient _httpClient;
private readonly HdTorrentsTrackerScraper _trackerScraper;
public RssProcessor(ApplicationDbContext context, ILogger<RssProcessor> logger,
HttpClient httpClient, HdTorrentsTrackerScraper trackerScraper)
{
_context = context;
_logger = logger;
_httpClient = httpClient;
_trackerScraper = trackerScraper;
}
public async Task ProcessRssFeeds()
{
var oneHourAgo = DateTime.UtcNow.AddHours(-1);
var activeFeeds = await _context.RssFeeds
.Where(f => f.IsActive && f.LastChecked <= oneHourAgo)
.ToListAsync();
foreach (var feed in activeFeeds)
{
try
{
await ProcessSingleFeed(feed);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing RSS feed {FeedName}", feed.Name);
}
}
}
private async Task ProcessSingleFeed(RssFeed feed)
{
var rssContent = await _httpClient.GetStringAsync(feed.Url);
var rssDoc = XDocument.Parse(rssContent);
var items = rssDoc.Descendants("item");
try
{
foreach (var item in items)
{
try
{
var torrentItem = ParseRssItem(item, feed);
Console.WriteLine($"Trying to save: Title='{torrentItem.Title}', InfoHash='{torrentItem.InfoHash}', MovieName='{torrentItem.MovieName}'");
// Kolla om den redan finns
var exists = await _context.TorrentItems
.AnyAsync(t => t.InfoHash == torrentItem.InfoHash ||
(t.Title == torrentItem.Title && t.RssSource == feed.Name));
if (!exists)
{
if (!string.IsNullOrEmpty(torrentItem.InfoHash) && !string.IsNullOrEmpty(torrentItem.DownloadKey))
{
var (seeders, leechers, completed) = await _trackerScraper.ScrapeHdTorrents(
torrentItem.InfoHash,
torrentItem.DownloadKey);
torrentItem.Seeders = seeders;
torrentItem.Leechers = leechers;
torrentItem.Completed = completed;
Console.WriteLine($"Scraped stats for {torrentItem.Title}: S:{seeders} L:{leechers} C:{completed}");
}
_context.TorrentItems.Add(torrentItem);
var savedChanges = await _context.SaveChangesAsync();
Console.WriteLine($"SaveChanges returned: {savedChanges}");
// Kolla auto-download regler
if (await ShouldAutoDownload(torrentItem))
{
await ProcessTorrentDownload(torrentItem);
}
} else
{
var howLongAgo = DateTime.UtcNow.AddHours(-6);
if (torrentItem.PublishDate >= howLongAgo)
{
var existing = await _context.TorrentItems
.FirstOrDefaultAsync(t => t.InfoHash == torrentItem.InfoHash
|| (t.Title == torrentItem.Title && t.RssSource == feed.Name));
if (existing != null)
{
var (seeders, leechers, completed) = await _trackerScraper.ScrapeHdTorrents(
existing.InfoHash,
existing.DownloadKey);
existing.Seeders = seeders;
existing.Leechers = leechers;
existing.Completed = completed;
_context.TorrentItems.Update(existing);
await _context.SaveChangesAsync();
Console.WriteLine($"Updated stats for {existing.Title}: S:{seeders} L:{leechers} C:{completed}");
}
}
}
}
catch (Exception itemEx)
{
Console.WriteLine($"Error processing individual item: {itemEx.Message}");
Console.WriteLine($"Stack trace: {itemEx.StackTrace}");
}
}
feed.LastChecked = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error in ProcessSingleFeed: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
}
}
private TorrentItem ParseRssItem(XElement item, RssFeed feed)
{
var title = item.Element("title")?.Value ?? "Unknown Title";
var description = item.Element("description")?.Value ?? ""; // Fix: Default till tom sträng
var pubDate = DateTime.TryParse(item.Element("pubDate")?.Value, out var date) ? date : DateTime.UtcNow;
var link = item.Element("link")?.Value ?? "";
var infoHash = ExtractInfoHashFromUrl(link);
var downloadKey = ExtractParameterFromUrl(link, "key");
var token = ExtractParameterFromUrl(link, "token");
var (movieName, year) = ParseMovieNameAndYear(title);
var magnetLink = "";
if (!string.IsNullOrEmpty(infoHash))
{
magnetLink = $"magnet:?xt=urn:btih:{infoHash}&dn={Uri.EscapeDataString(title)}";
}
return new TorrentItem
{
Title = title ?? "Unknown Title", // Garanterat inte null
Description = description ?? string.Empty, // Garanterat inte null
TorrentUrl = string.IsNullOrEmpty(link) ? null : link,
MagnetLink = string.IsNullOrEmpty(magnetLink) ? null : magnetLink,
InfoHash = string.IsNullOrEmpty(infoHash) ? null : infoHash,
PublishDate = pubDate,
RssSource = feed.Name ?? "Unknown", // Garanterat inte null
Status = TorrentStatus.New,
DownloadKey = string.IsNullOrEmpty(downloadKey) ? null : downloadKey,
Token = string.IsNullOrEmpty(token) ? null : token,
MovieName = string.IsNullOrEmpty(movieName) ? null : movieName,
Year = year,
Category = DetermineCategory(title),
CreatedAt = DateTime.UtcNow
};
}
private (string movieName, int? year) ParseMovieNameAndYear(string title)
{
// Exempel titlar:
// "Bring It on Fight to the Finish 2009 BluRay 1080p DDP 5.1 x264-hallowed"
// "Deadpool & Wolverine 2024 Hybrid 1080p UHD BluRay DD+5.1 Atmos DV HDR10+ x265-HiDt"
var movieName = "";
int? year = null;
// Leta efter år (4 siffror mellan 1900-2099)
var yearMatch = System.Text.RegularExpressions.Regex.Match(title, @"\b(19\d{2}|20\d{2})\b");
if (yearMatch.Success && int.TryParse(yearMatch.Groups[1].Value, out var parsedYear))
{
year = parsedYear;
// Ta allt före året som filmnamn
var yearIndex = yearMatch.Index;
movieName = title.Substring(0, yearIndex).Trim();
}
else
{
// Om inget år hittas, ta första delen före vanliga release-keywords
var keywordMatch = System.Text.RegularExpressions.Regex.Match(title,
@"\b(BluRay|WEB-DL|WEBRip|HDTV|DVDRIP|REMUX|UHD|1080p|720p|480p|2160p)\b",
RegexOptions.IgnoreCase);
if (keywordMatch.Success)
{
movieName = title.Substring(0, keywordMatch.Index).Trim();
}
else
{
// Fallback: ta första delen innan sista bindestreck
var lastDashIndex = title.LastIndexOf('-');
if (lastDashIndex > 0)
{
movieName = title.Substring(0, lastDashIndex).Trim();
}
else
{
movieName = title;
}
}
}
// Rensa bort vanliga suffix från filmnamnet
movieName = System.Text.RegularExpressions.Regex.Replace(movieName,
@"\b(REMASTERED|REPACK|PROPER|REAL|EXTENDED|DIRECTORS?\.?CUT|UNRATED)\b",
"", RegexOptions.IgnoreCase).Trim();
return (movieName, year);
}
private string ExtractInfoHashFromUrl(string url)
{
if (string.IsNullOrEmpty(url)) return "";
var match = System.Text.RegularExpressions.Regex.Match(url, @"hash=([a-fA-F0-9]{40})");
return match.Success ? match.Groups[1].Value.ToUpper() : "";
}
private string ExtractParameterFromUrl(string url, string paramName)
{
if (string.IsNullOrEmpty(url)) return "";
var match = System.Text.RegularExpressions.Regex.Match(url, $@"{paramName}=([^&]+)");
return match.Success ? Uri.UnescapeDataString(match.Groups[1].Value) : "";
}
private string DetermineCategory(string title)
{
if (System.Text.RegularExpressions.Regex.IsMatch(title, @"\b(S\d{2}E\d{2}|Season|Episode)\b", RegexOptions.IgnoreCase))
return "TV";
else
return "Movies";
}
private async Task<bool> ShouldAutoDownload(TorrentItem item)
{
var rules = await _context.DownloadRules.Where(r => r.AutoDownload).ToListAsync();
return rules.Any(rule =>
(string.IsNullOrEmpty(rule.KeywordFilter) ||
item.Title.Contains(rule.KeywordFilter, StringComparison.OrdinalIgnoreCase)) &&
(string.IsNullOrEmpty(rule.CategoryFilter) ||
item.Category == rule.CategoryFilter) &&
item.Seeders >= rule.MinSeeders &&
(rule.MaxSize == 0 || item.Size <= rule.MaxSize));
}
private async Task ProcessTorrentDownload(TorrentItem item)
{
try
{
if (!string.IsNullOrEmpty(item.MagnetLink))
{
_logger.LogInformation("Starting magnet download for {Title}", item.Title);
item.Status = TorrentStatus.Downloaded;
}
else if (!string.IsNullOrEmpty(item.TorrentUrl))
{
var torrentData = await _httpClient.GetByteArrayAsync(item.TorrentUrl);
item.Status = TorrentStatus.Downloaded;
}
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error downloading torrent {Title}", item.Title);
item.Status = TorrentStatus.Failed;
await _context.SaveChangesAsync();
}
}
}
}

View File

@@ -0,0 +1,41 @@
using Aberwyn.Data;
namespace Aberwyn.Services
{
public class TorrentRssService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<TorrentRssService> _logger;
public TorrentRssService(IServiceProvider serviceProvider, ILogger<TorrentRssService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Vänta lite innan första körningen
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _serviceProvider.CreateScope();
var rssProcessor = scope.ServiceProvider.GetRequiredService<IRssProcessor>();
await rssProcessor.ProcessRssFeeds();
_logger.LogInformation("RSS feeds processed successfully at {Time}", DateTime.UtcNow);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing RSS feeds");
}
// Vänta 10 minuter innan nästa körning
await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken);
}
}
}
}

View File

@@ -1,18 +1,22 @@
using Aberwyn.Data;
using BencodeNET.Objects;
using BencodeNET.Parsing;
using BencodeNET.Torrents;
using BencodeNET.Objects;
using Microsoft.EntityFrameworkCore;
using System.Text;
public interface ITorrentService
{
Task<TorrentInfo> ParseTorrentAsync(IFormFile file);
Task<TorrentInfo> FetchTrackerStatsAsync(TorrentInfo info);
Task<List<TorrentItem>> GetRecentTorrentsAsync(int count);
}
public class TorrentService : ITorrentService
{
private readonly HttpClient _httpClient;
private readonly ILogger<TorrentService> _logger;
private readonly ApplicationDbContext _context;
// Kända trackers och deras egenskaper
private readonly Dictionary<string, TrackerInfo> _knownTrackers = new()
@@ -27,13 +31,21 @@ public class TorrentService : ITorrentService
}
};
public TorrentService(HttpClient httpClient, ILogger<TorrentService> logger)
public TorrentService(HttpClient httpClient, ILogger<TorrentService> logger, ApplicationDbContext context)
{
_httpClient = httpClient;
_logger = logger;
_context = context;
_httpClient.Timeout = TimeSpan.FromSeconds(10);
}
public async Task<List<TorrentItem>> GetRecentTorrentsAsync(int count)
{
return await _context.TorrentItems
.OrderByDescending(t => t.PublishDate)
.Take(count)
.ToListAsync();
}
public async Task<TorrentInfo> ParseTorrentAsync(IFormFile file)
{
try
@@ -87,17 +99,31 @@ public class TorrentService : ITorrentService
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))
// Använd direkt byte array istället för att konvertera till sträng
if (TryGetStatsFromFiles(files, info.InfoHashBytes, info))
{
info.HasTrackerData = true;
return info;
}
info.ErrorMessage = "Info hash hittades inte i tracker-svaret";
}
else
// Om det inte fungerar, prova att URL-decode först
if (!string.IsNullOrEmpty(info.InfoHash))
{
info.ErrorMessage = "Inget 'files' objekt i tracker-svaret";
try
{
string decoded = Uri.UnescapeDataString(info.InfoHash);
byte[] decodedBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(decoded);
if (TryGetStatsFromFiles(files, decodedBytes, info))
{
info.HasTrackerData = true;
return info;
}
}
catch { /* Ignore decode errors */ }
}
info.ErrorMessage = "Info hash hittades inte i tracker-svaret";
}
}
catch (HttpRequestException ex)
@@ -118,10 +144,21 @@ public class TorrentService : ITorrentService
return info;
}
private bool TryGetStatsFromFiles(BDictionary files, string hashKey, TorrentInfo info)
private bool ByteArraysEqual(byte[] a, byte[] b)
{
if (files.TryGetValue(hashKey, out var hashEntry) && hashEntry is BDictionary stats)
if (a.Length != b.Length) return false;
for (int i = 0; i < a.Length; i++)
{
if (a[i] != b[i]) return false;
}
return true;
}
private bool TryGetStatsFromFiles(BDictionary files, byte[] hashBytes, TorrentInfo info)
{
// Skapa en BString från byte array
var bStringKey = new BString(hashBytes);
if (files.TryGetValue(bStringKey, out var hashEntry) && hashEntry is BDictionary stats)
{
info.Seeders = stats.TryGetInt("complete") ?? 0;
info.Leechers = stats.TryGetInt("incomplete") ?? 0;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddTorrentTables : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DownloadRules",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
KeywordFilter = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CategoryFilter = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MinSeeders = table.Column<int>(type: "int", nullable: false),
MaxSize = table.Column<long>(type: "bigint", nullable: false),
AutoDownload = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DownloadRules", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "RssFeeds",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Url = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
LastChecked = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RssFeeds", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "TorrentItems",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Title = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
MagnetLink = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
InfoHash = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
PublishDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Size = table.Column<long>(type: "bigint", nullable: false),
Seeders = table.Column<int>(type: "int", nullable: false),
Leechers = table.Column<int>(type: "int", nullable: false),
Status = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Category = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
RssSource = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Description = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
TorrentUrl = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_TorrentItems", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_TorrentItems_InfoHash",
table: "TorrentItems",
column: "InfoHash",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DownloadRules");
migrationBuilder.DropTable(
name: "RssFeeds");
migrationBuilder.DropTable(
name: "TorrentItems");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddTorrentTablesv2 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Completed",
table: "TorrentItems",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "DownloadKey",
table: "TorrentItems",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "MovieName",
table: "TorrentItems",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "Token",
table: "TorrentItems",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<int>(
name: "Year",
table: "TorrentItems",
type: "int",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Completed",
table: "TorrentItems");
migrationBuilder.DropColumn(
name: "DownloadKey",
table: "TorrentItems");
migrationBuilder.DropColumn(
name: "MovieName",
table: "TorrentItems");
migrationBuilder.DropColumn(
name: "Token",
table: "TorrentItems");
migrationBuilder.DropColumn(
name: "Year",
table: "TorrentItems");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class MakeTorrentFieldsNullable : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "TorrentUrl",
table: "TorrentItems",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "Token",
table: "TorrentItems",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "MovieName",
table: "TorrentItems",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "MagnetLink",
table: "TorrentItems",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "InfoHash",
table: "TorrentItems",
type: "varchar(255)",
nullable: true,
oldClrType: typeof(string),
oldType: "varchar(255)")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "DownloadKey",
table: "TorrentItems",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AlterColumn<string>(
name: "Category",
table: "TorrentItems",
type: "longtext",
nullable: true,
oldClrType: typeof(string),
oldType: "longtext")
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "TorrentItems",
keyColumn: "TorrentUrl",
keyValue: null,
column: "TorrentUrl",
value: "");
migrationBuilder.AlterColumn<string>(
name: "TorrentUrl",
table: "TorrentItems",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.UpdateData(
table: "TorrentItems",
keyColumn: "Token",
keyValue: null,
column: "Token",
value: "");
migrationBuilder.AlterColumn<string>(
name: "Token",
table: "TorrentItems",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.UpdateData(
table: "TorrentItems",
keyColumn: "MovieName",
keyValue: null,
column: "MovieName",
value: "");
migrationBuilder.AlterColumn<string>(
name: "MovieName",
table: "TorrentItems",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.UpdateData(
table: "TorrentItems",
keyColumn: "MagnetLink",
keyValue: null,
column: "MagnetLink",
value: "");
migrationBuilder.AlterColumn<string>(
name: "MagnetLink",
table: "TorrentItems",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.UpdateData(
table: "TorrentItems",
keyColumn: "InfoHash",
keyValue: null,
column: "InfoHash",
value: "");
migrationBuilder.AlterColumn<string>(
name: "InfoHash",
table: "TorrentItems",
type: "varchar(255)",
nullable: false,
oldClrType: typeof(string),
oldType: "varchar(255)",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.UpdateData(
table: "TorrentItems",
keyColumn: "DownloadKey",
keyValue: null,
column: "DownloadKey",
value: "");
migrationBuilder.AlterColumn<string>(
name: "DownloadKey",
table: "TorrentItems",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
migrationBuilder.UpdateData(
table: "TorrentItems",
keyColumn: "Category",
keyValue: null,
column: "Category",
value: "");
migrationBuilder.AlterColumn<string>(
name: "Category",
table: "TorrentItems",
type: "longtext",
nullable: false,
oldClrType: typeof(string),
oldType: "longtext",
oldNullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
.OldAnnotation("MySql:CharSet", "utf8mb4");
}
}
}

View File

@@ -697,6 +697,34 @@ namespace Aberwyn.Migrations
b.ToTable("WeeklyMenu", (string)null);
});
modelBuilder.Entity("DownloadRule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<bool>("AutoDownload")
.HasColumnType("tinyint(1)");
b.Property<string>("CategoryFilter")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("KeywordFilter")
.IsRequired()
.HasColumnType("longtext");
b.Property<long>("MaxSize")
.HasColumnType("bigint");
b.Property<int>("MinSeeders")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("DownloadRules");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
@@ -825,6 +853,103 @@ namespace Aberwyn.Migrations
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("RssFeed", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("LastChecked")
.HasColumnType("datetime(6)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("RssFeeds");
});
modelBuilder.Entity("TorrentItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Category")
.HasColumnType("longtext");
b.Property<int>("Completed")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("DownloadKey")
.HasColumnType("longtext");
b.Property<string>("InfoHash")
.HasColumnType("varchar(255)");
b.Property<int>("Leechers")
.HasColumnType("int");
b.Property<string>("MagnetLink")
.HasColumnType("longtext");
b.Property<string>("MovieName")
.HasColumnType("longtext");
b.Property<DateTime>("PublishDate")
.HasColumnType("datetime(6)");
b.Property<string>("RssSource")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Seeders")
.HasColumnType("int");
b.Property<long>("Size")
.HasColumnType("bigint");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Token")
.HasColumnType("longtext");
b.Property<string>("TorrentUrl")
.HasColumnType("longtext");
b.Property<int?>("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")

View File

@@ -1,3 +1,5 @@
using System.ComponentModel.DataAnnotations;
public class TorrentInfo
{
public string FileName { get; set; }
@@ -27,3 +29,59 @@ public class TorrentUploadViewModel
public TorrentInfo TorrentInfo { get; set; }
public bool ShowResults { get; set; } = false;
}
public class RssFeed
{
public int Id { get; set; }
public string Url { get; set; }
public string Name { get; set; }
public DateTime LastChecked { get; set; }
public bool IsActive { get; set; }
}
public class TorrentItem
{
public int Id { get; set; }
[Required]
public string Title { get; set; } = string.Empty;
public string? MagnetLink { get; set; }
public string? InfoHash { get; set; }
public DateTime PublishDate { get; set; }
public long Size { get; set; } = 0;
public int Seeders { get; set; } = 0;
public int Leechers { get; set; } = 0;
public int Completed { get; set; } = 0;
public TorrentStatus Status { get; set; } = TorrentStatus.New;
public string? Category { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Required]
public string RssSource { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string? TorrentUrl { get; set; }
public string? MovieName { get; set; }
public int? Year { get; set; }
public string? DownloadKey { get; set; }
public string? Token { get; set; }
}
public enum TorrentStatus
{
New,
Downloaded,
Processing,
Completed,
Failed,
Ignored
}
public class DownloadRule
{
public int Id { get; set; }
public string KeywordFilter { get; set; }
public string CategoryFilter { get; set; }
public int MinSeeders { get; set; }
public long MaxSize { get; set; }
public bool AutoDownload { get; set; }
}

View File

@@ -66,6 +66,13 @@ catch (Exception ex)
builder.Services.AddHttpClient<ITorrentService, TorrentService>();
builder.Services.AddScoped<ITorrentService, TorrentService>();
builder.Services.AddHttpClient<RssProcessor>();
builder.Services.AddScoped<IRssProcessor, RssProcessor>();
builder.Services.AddHostedService<TorrentRssService>();
builder.Services.AddHttpClient<HdTorrentsTrackerScraper>();
builder.Services.AddScoped<HdTorrentsTrackerScraper>();
// Add services to the container
builder.Services.AddControllersWithViews()
@@ -166,7 +173,12 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
options.SupportedUICultures = supportedCultures;
});
builder.Services.AddSingleton<SetupService>();
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
// Eller om du vill ha mer detaljerad loggning:
builder.Logging.SetMinimumLevel(LogLevel.Information);
var app = builder.Build();
app.UseStaticFiles();

View File

@@ -47,6 +47,10 @@
@if (User.IsInRole("Budget"))
{
<li><a asp-controller="Budget" asp-action="Index"> Budget</a></li>
}
@if (User.IsInRole("Admin"))
{
<li><a asp-controller="torrent" asp-action="Index"> Torrents</a></li>
}
@if (User.IsInRole("Chef"))
{

View File

@@ -1,225 +1,23 @@
@model TorrentUploadViewModel
@{
ViewData["Title"] = "Torrent Analyzer";
}
@model IEnumerable<TorrentItem>
<link rel="stylesheet" href="~/css/torrent.css" />
<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 class="torrent-list">
<div class="torrent-header">
<div class="col-title">Titel</div>
<div class="col-center">Seeders</div>
<div class="col-center">Leechers</div>
<div class="col-center">Completed</div>
<div class="col-right">Datum</div>
</div>
<div class="card-body">
@if (!ViewData.ModelState.IsValid)
@foreach (var t in Model)
{
<div class="alert alert-danger">
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
{
<div>@error.ErrorMessage</div>
}
<div class="torrent-row">
<div class="col-title">@t.Title</div>
<div class="col-center @(t.Seeders > 40 ? "highlight-green" : "")">@t.Seeders</div>
<div class="col-center highlight-red">@t.Leechers</div>
<div class="col-center">@t.Completed</div>
<div class="col-right">@t.PublishDate.ToString("yyyy-MM-dd HH:mm")</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>

View File

@@ -0,0 +1,61 @@
/* ==========================================================
TORRENT LIST STYLING
========================================================== */
.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;
color: #1F2C3C;
}
.torrent-header, .torrent-row {
display: grid;
grid-template-columns: 3fr 1fr 1fr 1fr 2fr;
align-items: center;
padding: 6px 0;
}
.torrent-header {
font-weight: 600;
border-bottom: 2px solid #ccc;
margin-bottom: 4px;
color: #223344;
}
.torrent-row {
border-bottom: 1px solid #e0e0e0;
transition: background-color 0.2s;
}
.torrent-row:hover {
background-color: #f7f7f7;
}
.col-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.col-center {
text-align: center;
}
.col-right {
text-align: right;
font-size: 12px;
color: #666;
}
.highlight-green {
color: #00cc66;
font-weight: bold;
}
.highlight-red {
color: #cc3333;
}