.NET client for the Inoreader HTTP API
- Prerequisites
- Installation
- Features
- Configuration
- Usage
- List articles
- Check if there are new articles
- Mark an article read or unread
- Star or unstar an article
- Tag or untag an article
- Subscribe to a feed
- Show the number of unread articles
- List subscriptions, folders, or tags
- Rename subscriptions, folders, or tags
- Organize subscriptions into folders
- Delete subscriptions, folders, or tags
- Mark all articles as read
- Get self user
- Any .NET runtime that is compatible with .NET Standard 2.0
- .NET 5 or later
- .NET Core 2.0 or later
- .NET Framework 4.6.2 or later
- Inoreader account
dotnet add package InoreaderCsLike IsaacSchemm/InoreaderFs, but not fucked up:
- Correctly send the
otquery parameter tostream/items/idsin microsecond format, not seconds, so it isn't ignored. - Has a built-in OAuth2 client with smart refresh logic as well as a password-based auth client, both of which make auth requests automatically and support pluggable persistence strategies.
- Observe rate-limiting statistics.
- Uses modern, interchangeable, customizable
HttpClientinstead of ancient, disgustingHttpWebRequest. - Allows you to set custom HTTP request headers, such as
User-Agent. - Easily gets article's read and starred state, short ID, description, and original feed name and URL.
- Instances are configurable because they are not static classes, so you don't need to supply authentication to literally every request, you can just set it up once, for example in an IoC context, and not have to pass it around your entire codebase.
- Interfaces allow mocking and interchangeability, instead of everything being sealed static classes.
- Hierarchical interface structure makes it easier to find the API method you want and understand what it applies to.
- Facade pattern hides the complexity of the Inoreader API's very overloaded methods with lots of conditionally valid parameters.
- Avoids the insane concept of stream IDs and all their complexity of parsing, translating, handling, and using them, because developers today shouldn't have to deal with weird Google decisions from 2001 that Inoreader bent over backwards to be compatible with for no real benefit because there is no Google Reader client that anyone pointed at Inoreader as a drop-in replacement backend.
- Full documentation of methods and entities.
- Exceptions have information about what went wrong.
- Updated in the last 6 years by someone who uses Inoreader and this library heavily every day.
- Automated tests.
- Includes correct, strongly-typed arguments in API methods like
subscription/edit, which are incorrect in the official documentation and unehlpful, weakly-typed strings in InoreaderFs.
Implement the IAuthTokenPersister interface so that auth tokens can be saved and loaded.
public class MyAuthTokenPersister: IAuthTokenPersister {
public async Task<PersistedAuthTokens?> LoadAuthTokens() {
// load auth tokens
}
public async Task SaveAuthTokens(PersistedAuthTokens authToken) {
// save auth tokens
}
}If your app authenticates to Inoreader using OAuth2, subclass the OAuth2Client abstract class so that users can see and grant consent for OAuth2 app access to their account.
public class MyOauth2Client(Oauth2Parameters oauthParameters, IAuthTokenPersister authTokenPersister, IHttpClient? httpClient, ILoggerFactory? loggerFactory)
: Oauth2Client(oauthParameters, authTokenPersister, httpClient, loggerFactory) {
protected override Uri AuthorizationReceiverCallbackUrl => /* URL of your web server's OAuth2 callback resource */;
protected override async Task<ConsentResult> ShowConsentPageToUser(Uri consentUri, Uri codeReceiverUri, Task authorizationSuccess) {
// show consent page to user
}
}Otherwise, construct a PasswordAuthClient instance.
Create a new instance of InoreaderClient. It uses one instance of an IAuthClient, so if you have multiple auth strategies, construct additional InoreaderClient instances.
Oauth2Parameters oauthParameters = new(appId: 123, appKey: "abc");
using IInoreaderClient inoreader = new InoreaderClient(new InoreaderOptions {
AuthClient = new MyOauth2Client(oauthParameters, new MyAuthTokenPersister())
});DetailedArticles articleList = await inoreader.Newsfeed.ListArticlesDetailed();
foreach (Article article in articleList.Articles) {
Console.WriteLine(article.Title);
}Useful options:
- To only fetch articles from a specific folder, tag, or subscription/feed, replace
Newsfeed(which refers to all articles in the entire account) withFolders,Tags, orSubscriptions, respectively. - To change the page size, pass the
maxArticlesparameter. - To only return articles crawled after a certain time, pass the
minTimeparameter. - To remove or require articles to have a specific state like read or starred, pass the
subtractorintersectparameters, respectively. - To fetch subsequent pages from a multi-page response, pass the previous page or its
PaginationTokenas thepaginationparameter.
Caution
Even when the pagination parameter is sent properly, the Inoreader API servers sometimes ignore it and incorrectly return the first page again instead of the desired page. This makes it look like many of the articles are duplicates. After fetching multiple pages of articles, always filter by the unique Article.ShortId to remove the erroneous duplicates, for example, using IEnumerable<T>.Distinct.
DateTimeOffset mostRecentArticleTime = default;
while (true) {
BriefArticles mostRecentArticle = await inoreader.Newsfeed.ListArticlesBrief(maxArticles: 1, minTime: mostRecentArticleTime);
if (mostRecentArticle.Articles[0].CrawlTime is var articleTime && articleTime != mostRecentArticleTime) {
mostRecentArticleTime = articleTime;
Console.WriteLine("New article received");
}
await Task.Delay(TimeSpan.FromMinutes(10));
}await inoreader.Articles.MarkArticles(ArticleState.Read, [article]);await inoreader.Articles.UnmarkArticles(ArticleState.Read, [article]);await inoreader.Articles.MarkArticles(ArticleState.Starred, [article]);await inoreader.Articles.UnmarkArticles(ArticleState.Starred, [article]);await inoreader.Articles.TagArticles("my tag", [article]);await inoreader.Articles.UntagArticles("my tag", [article]);Uri feedUrl = new("https://arstechnica.com/science/feed/");
SubscriptionCreationResult subscription = await inoreader.Subscriptions.Subscribe(feedUrl, CancellationToken.None);
// alternative that immediately sets name or folder
await inoreader.Subscriptions.Subscribe(feed, title: "Science", folder: "Technology");NewsfeedUnreadCounts unreadResponse = await inoreader.Newsfeed.GetUnreadCounts();
int unreadCount = unreadResponse.AllArticles.UnreadCount;
string unreadLabel = $"{unreadCount:N0}{(unreadCount == unreadResponse.MaxDisplayableCount ? "+" : "")}";IEnumerable<Subscription> subscriptions = await inoreader.Subscriptions.List();
IEnumerable<FolderState> folders = await inoreader.Folders.List();
IEnumerable<TagState> tags = await inoreader.Tags.List();Uri subscription = new("https://arstechnica.com/science/feed/");
await inoreader.Subscriptions.Rename(subscription, newName: "Science!");
await inoreader.Folders.Rename("Technology", newName: "New Technology");
await inoreader.Tags.Rename("My tag", newName: "My new tag");Uri subscription = new("https://arstechnica.com/science/feed/");
await inoreader.Subscriptions.AddToFolder(subscription, "Technology");
await inoreader.Subscriptions.RemoveFromFolder(subscription, "Technology");await inoreader.Subscriptions.Unsubscribe(new Uri("https://arstechnica.com/science/feed/"));
await inoreader.Folders.Delete("Technology");
await inoreader.Tags.Delete("My tag");Article latestArticle;
await inoreader.Newsfeed.MarkAllArticlesAsRead(latestArticle.CrawlTime);
await inoreader.Folders.MarkAllArticlesAsRead("Technology", latestArticle.CrawlTime);
await inoreader.Tags.MarkAllArticlesAsRead("My tag", latestArticle.CrawlTime);
await inoreader.Subscriptions.MarkAllArticlesAsRead(new Uri("https://arstechnica.com/science/feed/"), latestArticle.CrawlTime);User self = await inoreader.Users.GetSelf();