This commit is contained in:
@@ -12,9 +12,10 @@ public class TorrentController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[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]
|
[HttpPost]
|
||||||
@@ -96,4 +97,19 @@ public class TorrentController : Controller
|
|||||||
return Json(new { success = false, error = "Fel vid uppdatering" });
|
return Json(new { success = false, error = "Fel vid uppdatering" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> List()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var torrents = await _torrentService.GetRecentTorrentsAsync(50);
|
||||||
|
return View(torrents);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Fel vid hämtning av torrent-lista");
|
||||||
|
return View(new List<TorrentItem>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,13 @@ namespace Aberwyn.Data
|
|||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(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<WeeklyMenu>().ToTable("WeeklyMenu");
|
||||||
builder.Entity<MealCategory>().HasData(
|
builder.Entity<MealCategory>().HasData(
|
||||||
new MealCategory
|
new MealCategory
|
||||||
@@ -52,7 +58,9 @@ namespace Aberwyn.Data
|
|||||||
public DbSet<LabIngredient> LabIngredients { get; set; }
|
public DbSet<LabIngredient> LabIngredients { get; set; }
|
||||||
public DbSet<LabVersionIngredient> LabVersionIngredients { get; set; }
|
public DbSet<LabVersionIngredient> LabVersionIngredients { get; set; }
|
||||||
public DbSet<MealRating> MealRatings { 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,19 +1,23 @@
|
|||||||
|
using Aberwyn.Data;
|
||||||
|
using BencodeNET.Objects;
|
||||||
using BencodeNET.Parsing;
|
using BencodeNET.Parsing;
|
||||||
using BencodeNET.Torrents;
|
using BencodeNET.Torrents;
|
||||||
using BencodeNET.Objects;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
public interface ITorrentService
|
public interface ITorrentService
|
||||||
{
|
{
|
||||||
Task<TorrentInfo> ParseTorrentAsync(IFormFile file);
|
Task<TorrentInfo> ParseTorrentAsync(IFormFile file);
|
||||||
Task<TorrentInfo> FetchTrackerStatsAsync(TorrentInfo info);
|
Task<TorrentInfo> FetchTrackerStatsAsync(TorrentInfo info);
|
||||||
|
Task<List<TorrentItem>> GetRecentTorrentsAsync(int count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TorrentService : ITorrentService
|
public class TorrentService : ITorrentService
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger<TorrentService> _logger;
|
private readonly ILogger<TorrentService> _logger;
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
// Kända trackers och deras egenskaper
|
// Kända trackers och deras egenskaper
|
||||||
private readonly Dictionary<string, TrackerInfo> _knownTrackers = new()
|
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;
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_context = context;
|
||||||
_httpClient.Timeout = TimeSpan.FromSeconds(10);
|
_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)
|
public async Task<TorrentInfo> ParseTorrentAsync(IFormFile file)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -87,18 +99,32 @@ public class TorrentService : ITorrentService
|
|||||||
|
|
||||||
if (bdict.TryGetValue("files", out var filesValue) && filesValue is BDictionary files)
|
if (bdict.TryGetValue("files", out var filesValue) && filesValue is BDictionary files)
|
||||||
{
|
{
|
||||||
if (TryGetStatsFromFiles(files, info.InfoHash, info) ||
|
// Använd direkt byte array istället för att konvertera till sträng
|
||||||
TryGetStatsFromFiles(files, Encoding.UTF8.GetString(info.InfoHashBytes), info))
|
if (TryGetStatsFromFiles(files, info.InfoHashBytes, info))
|
||||||
{
|
{
|
||||||
info.HasTrackerData = true;
|
info.HasTrackerData = true;
|
||||||
return info;
|
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";
|
info.ErrorMessage = "Info hash hittades inte i tracker-svaret";
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
info.ErrorMessage = "Inget 'files' objekt i tracker-svaret";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
@@ -118,10 +144,21 @@ public class TorrentService : ITorrentService
|
|||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
private bool ByteArraysEqual(byte[] a, byte[] b)
|
||||||
private bool TryGetStatsFromFiles(BDictionary files, string hashKey, TorrentInfo info)
|
|
||||||
{
|
{
|
||||||
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.Seeders = stats.TryGetInt("complete") ?? 0;
|
||||||
info.Leechers = stats.TryGetInt("incomplete") ?? 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);
|
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 =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
@@ -825,6 +853,103 @@ namespace Aberwyn.Migrations
|
|||||||
b.ToTable("AspNetUserTokens", (string)null);
|
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 =>
|
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")
|
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
public class TorrentInfo
|
public class TorrentInfo
|
||||||
{
|
{
|
||||||
public string FileName { get; set; }
|
public string FileName { get; set; }
|
||||||
@@ -26,4 +28,60 @@ public class TorrentUploadViewModel
|
|||||||
public IFormFile TorrentFile { get; set; }
|
public IFormFile TorrentFile { get; set; }
|
||||||
public TorrentInfo TorrentInfo { get; set; }
|
public TorrentInfo TorrentInfo { get; set; }
|
||||||
public bool ShowResults { get; set; } = false;
|
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.AddHttpClient<ITorrentService, TorrentService>();
|
||||||
builder.Services.AddScoped<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
|
// Add services to the container
|
||||||
builder.Services.AddControllersWithViews()
|
builder.Services.AddControllersWithViews()
|
||||||
@@ -166,7 +173,12 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
|
|||||||
options.SupportedUICultures = supportedCultures;
|
options.SupportedUICultures = supportedCultures;
|
||||||
});
|
});
|
||||||
builder.Services.AddSingleton<SetupService>();
|
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();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|||||||
@@ -47,6 +47,10 @@
|
|||||||
@if (User.IsInRole("Budget"))
|
@if (User.IsInRole("Budget"))
|
||||||
{
|
{
|
||||||
<li><a asp-controller="Budget" asp-action="Index"> Budget</a></li>
|
<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"))
|
@if (User.IsInRole("Chef"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,225 +1,23 @@
|
|||||||
@model TorrentUploadViewModel
|
@model IEnumerable<TorrentItem>
|
||||||
@{
|
<link rel="stylesheet" href="~/css/torrent.css" />
|
||||||
ViewData["Title"] = "Torrent Analyzer";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="torrent-list">
|
||||||
<div class="row justify-content-center">
|
<div class="torrent-header">
|
||||||
<div class="col-md-8">
|
<div class="col-title">Titel</div>
|
||||||
<div class="card">
|
<div class="col-center">Seeders</div>
|
||||||
<div class="card-header">
|
<div class="col-center">Leechers</div>
|
||||||
<h3 class="mb-0">
|
<div class="col-center">Completed</div>
|
||||||
<i class="fas fa-download"></i> Torrent Analyzer
|
<div class="col-right">Datum</div>
|
||||||
</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>
|
||||||
</div>
|
|
||||||
|
|
||||||
@functions {
|
@foreach (var t in Model)
|
||||||
string FormatFileSize(long bytes)
|
|
||||||
{
|
{
|
||||||
if (bytes == 0) return "Okänd storlek";
|
<div class="torrent-row">
|
||||||
|
<div class="col-title">@t.Title</div>
|
||||||
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
|
<div class="col-center @(t.Seeders > 40 ? "highlight-green" : "")">@t.Seeders</div>
|
||||||
int order = 0;
|
<div class="col-center highlight-red">@t.Leechers</div>
|
||||||
double size = bytes;
|
<div class="col-center">@t.Completed</div>
|
||||||
|
<div class="col-right">@t.PublishDate.ToString("yyyy-MM-dd HH:mm")</div>
|
||||||
while (size >= 1024 && order < sizes.Length - 1)
|
</div>
|
||||||
{
|
|
||||||
order++;
|
|
||||||
size = size / 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{size:0.##} {sizes[order]}";
|
|
||||||
}
|
}
|
||||||
}
|
</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>
|
|
||||||
|
|||||||
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