This commit is contained in:
@@ -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>());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
103
Aberwyn/Data/HDTorrentsTrackerScraper.cs
Normal file
103
Aberwyn/Data/HDTorrentsTrackerScraper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
Aberwyn/Data/IRssProcessor.cs
Normal file
7
Aberwyn/Data/IRssProcessor.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Aberwyn.Data
|
||||
{
|
||||
public interface IRssProcessor
|
||||
{
|
||||
Task ProcessRssFeeds();
|
||||
}
|
||||
}
|
||||
286
Aberwyn/Data/RssProcessor.cs
Normal file
286
Aberwyn/Data/RssProcessor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Aberwyn/Data/TorrentRssService.cs
Normal file
41
Aberwyn/Data/TorrentRssService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,18 +99,32 @@ 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;
|
||||
}
|
||||
|
||||
// Om det inte fungerar, prova att URL-decode först
|
||||
if (!string.IsNullOrEmpty(info.InfoHash))
|
||||
{
|
||||
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";
|
||||
}
|
||||
else
|
||||
{
|
||||
info.ErrorMessage = "Inget 'files' objekt 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;
|
||||
|
||||
1177
Aberwyn/Migrations/20250805202224_AddTorrentTables.Designer.cs
generated
Normal file
1177
Aberwyn/Migrations/20250805202224_AddTorrentTables.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
105
Aberwyn/Migrations/20250805202224_AddTorrentTables.cs
Normal file
105
Aberwyn/Migrations/20250805202224_AddTorrentTables.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
1195
Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.Designer.cs
generated
Normal file
1195
Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
69
Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.cs
Normal file
69
Aberwyn/Migrations/20250806065232_AddTorrentTablesv2.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
1188
Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.Designer.cs
generated
Normal file
1188
Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
211
Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.cs
Normal file
211
Aberwyn/Migrations/20250806072233_MakeTorrentFieldsNullable.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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"))
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
<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 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>
|
||||
|
||||
@functions {
|
||||
string FormatFileSize(long bytes)
|
||||
@foreach (var t in Model)
|
||||
{
|
||||
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]}";
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
61
Aberwyn/wwwroot/css/torrent.css
Normal file
61
Aberwyn/wwwroot/css/torrent.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user