More changes to add Torrents automatically; Modified cache to 8 days; Modified season finale lookups for edge case
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace AnimeAnnouncer.Torrents
|
||||||
|
{
|
||||||
|
public record NyaaRssItem(
|
||||||
|
String Title,
|
||||||
|
String Published,
|
||||||
|
String Category,
|
||||||
|
String CategoryID,
|
||||||
|
String Size,
|
||||||
|
String Link,
|
||||||
|
String Description,
|
||||||
|
String PostID);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads Nyaa RSS feeds and downloads the .torrent files they link to.
|
||||||
|
/// </summary>
|
||||||
|
public class NyaaRssReader
|
||||||
|
{
|
||||||
|
private readonly HttpClient httpClient;
|
||||||
|
|
||||||
|
public NyaaRssReader(HttpClient? httpClient = null)
|
||||||
|
{
|
||||||
|
this.httpClient = httpClient ?? new HttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a Nyaa search URL into its RSS equivalent, if it isn't one already.
|
||||||
|
/// </summary>
|
||||||
|
public static String ToRssUrl(String url)
|
||||||
|
{
|
||||||
|
return url.Contains("rss") ? url : url + "&page=rss";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<NyaaRssItem>> ReadAsync(String url)
|
||||||
|
{
|
||||||
|
var response = await httpClient.GetStringAsync(ToRssUrl(url));
|
||||||
|
return Parse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<NyaaRssItem> Parse(String rssXml)
|
||||||
|
{
|
||||||
|
XDocument doc = XDocument.Parse(rssXml);
|
||||||
|
XNamespace nyaaNS = doc.Root!.GetNamespaceOfPrefix("nyaa") ?? XNamespace.None;
|
||||||
|
return (from item in doc.Descendants("channel").Elements("item")
|
||||||
|
select new NyaaRssItem(
|
||||||
|
Title: item.Element("title")?.Value ?? String.Empty,
|
||||||
|
Published: item.Element("pubDate")?.Value ?? String.Empty,
|
||||||
|
Category: item.Element(nyaaNS + "category")?.Value ?? String.Empty,
|
||||||
|
CategoryID: item.Element(nyaaNS + "categoryId")?.Value ?? String.Empty,
|
||||||
|
Size: item.Element(nyaaNS + "size")?.Value ?? String.Empty,
|
||||||
|
Link: item.Element("link")?.Value ?? String.Empty,
|
||||||
|
Description: item.Element("description")?.Value ?? String.Empty,
|
||||||
|
PostID: item.Element("guid")?.Value ?? String.Empty
|
||||||
|
)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<byte[]> DownloadTorrentAsync(NyaaRssItem item)
|
||||||
|
{
|
||||||
|
return httpClient.GetByteArrayAsync(item.Link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace AnimeAnnouncer.Torrents
|
||||||
|
{
|
||||||
|
public static class TorrentDirectoryNamer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Turns a release title into a dotted directory name, stripping the episode number
|
||||||
|
/// (keeping the resolution) and any trailing parenthesised suffix.
|
||||||
|
/// e.g. "Show Name S01E05 1080p WEB (Group)" -> "Show.Name.S01.1080p.WEB"
|
||||||
|
/// </summary>
|
||||||
|
public static String FromTitle(String title)
|
||||||
|
{
|
||||||
|
String name = title.Trim();
|
||||||
|
name = Regex.Replace(name, @"E\d{2,3}.*?(\d{3,4}p)", " $1");
|
||||||
|
name = Regex.Replace(name, @"\s\(.*?\)$", "");
|
||||||
|
return name.Replace(" ", ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String PrepareForURI(String title)
|
||||||
|
{
|
||||||
|
var titlePart = Regex.Replace(title, @"[^\w\s]+", String.Empty);
|
||||||
|
titlePart = Regex.Replace(titlePart, @"\s+", "+");
|
||||||
|
return titlePart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the full save path for a release title under <paramref name="destinationFolder"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static String FromTitle(String title, String destinationFolder)
|
||||||
|
{
|
||||||
|
return destinationFolder + (destinationFolder.EndsWith("/") ? String.Empty : "/") + FromTitle(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using Transmission.API.RPC;
|
||||||
|
using Transmission.API.RPC.Arguments;
|
||||||
|
using Transmission.API.RPC.Entity;
|
||||||
|
|
||||||
|
namespace AnimeAnnouncer.Torrents
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Thin wrapper around the Transmission RPC client for adding torrents to the server.
|
||||||
|
/// </summary>
|
||||||
|
public class TransmissionService
|
||||||
|
{
|
||||||
|
public Client Client { get; }
|
||||||
|
|
||||||
|
private TransmissionService(Client client)
|
||||||
|
{
|
||||||
|
Client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a client and verifies the server is reachable by requesting session information.
|
||||||
|
/// Throws <see cref="InvalidOperationException"/> if the server doesn't respond with a valid session.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<TransmissionService> ConnectAsync(String host, String? username, String? password)
|
||||||
|
{
|
||||||
|
var client = new Client(host, null, username, password);
|
||||||
|
var sessionInformation = await client.GetSessionInformationAsync();
|
||||||
|
if (sessionInformation == null || String.IsNullOrEmpty(sessionInformation.Version))
|
||||||
|
throw new InvalidOperationException($"An error occurred while receiving session information from the rpc server at {host}");
|
||||||
|
|
||||||
|
return new TransmissionService(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a .torrent file to the server, saving to <paramref name="saveLocation"/>, and applies a seed ratio limit.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<NewTorrentInfo> AddNewTorrentFileToServerAsync(byte[] torrentFileData, String saveLocation, bool paused = true, double seedRatioLimit = 1.0)
|
||||||
|
{
|
||||||
|
NewTorrent newTorrent = new()
|
||||||
|
{
|
||||||
|
DownloadDirectory = saveLocation,
|
||||||
|
Paused = paused,
|
||||||
|
Metainfo = Convert.ToBase64String(torrentFileData)
|
||||||
|
};
|
||||||
|
var torrentInfo = await Client.TorrentAddAsync(newTorrent);
|
||||||
|
var seedSettings = new TorrentSettings()
|
||||||
|
{
|
||||||
|
SeedRatioLimit = seedRatioLimit,
|
||||||
|
SeedRatioMode = 1,
|
||||||
|
IDs = [torrentInfo.ID]
|
||||||
|
};
|
||||||
|
|
||||||
|
// TorrentSetAsync returns void in Transmission.API.RPC 2.1.2, so it can't be awaited
|
||||||
|
Client.TorrentSet(seedSettings);
|
||||||
|
return torrentInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,6 +65,10 @@ public class IndexModel : PageModel
|
|||||||
_logger.LogWarning(ex, "Failed to retrieve Anilist cache item for title: {Title}", airingItem.Title);
|
_logger.LogWarning(ex, "Failed to retrieve Anilist cache item for title: {Title}", airingItem.Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With no details from either source there is nothing worth displaying.
|
||||||
|
if (cacheItem == null && anilistItem == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
viewModels.Add(new AiringShowViewModel
|
viewModels.Add(new AiringShowViewModel
|
||||||
{
|
{
|
||||||
AiringSoon = airingItem,
|
AiringSoon = airingItem,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ namespace AnimeAnnouncer
|
|||||||
Console.WriteLine("Using cache");
|
Console.WriteLine("Using cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
tmdbCache.CacheExpiration = TimeSpan.FromDays(7);
|
tmdbCache.CacheExpiration = TimeSpan.FromDays(8);
|
||||||
|
|
||||||
String mastodonInstance = config["MastodonInstance"];
|
String mastodonInstance = config["MastodonInstance"];
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ namespace AnimeAnnouncer
|
|||||||
{
|
{
|
||||||
var titleSeason = int.Parse(season);
|
var titleSeason = int.Parse(season);
|
||||||
bool seasonOverride = false;
|
bool seasonOverride = false;
|
||||||
if(latestSeasonNumber ==1 && titleSeason > 1 && lastEpisodeNumber > 3)
|
if(latestSeasonNumber < titleSeason && titleSeason > 1 && lastEpisodeNumber > 3)
|
||||||
{ //Check episode groups to see if this is a single-season default show
|
{ //Check episode groups to see if this is a single-season default show
|
||||||
var showResult = await tmdbClient.GetTvShowAsync(supposedShowId, TMDbLib.Objects.TvShows.TvShowMethods.EpisodeGroups);
|
var showResult = await tmdbClient.GetTvShowAsync(supposedShowId, TMDbLib.Objects.TvShows.TvShowMethods.EpisodeGroups);
|
||||||
var seasonEpisodeGroup = showResult.EpisodeGroups.Results.FirstOrDefault(eg => eg.Name == "Seasons");
|
var seasonEpisodeGroup = showResult.EpisodeGroups.Results.FirstOrDefault(eg => eg.Name == "Seasons");
|
||||||
@@ -419,10 +419,10 @@ namespace AnimeAnnouncer
|
|||||||
_ = tmdbCache.SetPair($"ShowAnnounced-{supposedShowId}", latestSeasonNumber.ToString());
|
_ = tmdbCache.SetPair($"ShowAnnounced-{supposedShowId}", latestSeasonNumber.ToString());
|
||||||
_ = PostStatus(cachedShow);
|
_ = PostStatus(cachedShow);
|
||||||
var existingAiringItem = airingSoonList.FirstOrDefault(s => s.ShowID == cachedShow.ShowID);
|
var existingAiringItem = airingSoonList.FirstOrDefault(s => s.ShowID == cachedShow.ShowID);
|
||||||
if (existingAiringItem != null && existingAiringItem.Download)
|
if (existingAiringItem is { Download: true })
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Attempting to queue {existingAiringItem.Title} in the torrent client");
|
Console.WriteLine($"Attempting to queue {existingAiringItem.Title} in the torrent client");
|
||||||
_ = AddToTorrentClient(cachedShow);
|
await AddToTorrentClient(cachedShow);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -449,6 +449,7 @@ namespace AnimeAnnouncer
|
|||||||
}
|
}
|
||||||
|
|
||||||
var folderName = TorrentDirectoryNamer.FromTitle(items.First().Title, destinationFolder);
|
var folderName = TorrentDirectoryNamer.FromTitle(items.First().Title, destinationFolder);
|
||||||
|
Console.WriteLine($"Adding {folderName} to transmission");
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user