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