feed finder
This commit is contained in:
@@ -1,16 +1,24 @@
|
|||||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart';
|
import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart';
|
||||||
|
|
||||||
part 'web_page_info.g.dart';
|
part 'web_page_info.g.dart';
|
||||||
|
|
||||||
@CopyWith()
|
@CopyWith()
|
||||||
class WebPageInfo {
|
class WebPageInfo with FastEquatable {
|
||||||
final Uri url;
|
final Uri url;
|
||||||
final String? title;
|
final String? title;
|
||||||
final BrowserIcon? favicon;
|
final BrowserIcon? favicon;
|
||||||
|
final Set<String>? feeds;
|
||||||
|
|
||||||
bool get isPageInfoComplete =>
|
bool get isPageInfoComplete =>
|
||||||
(title?.isNotEmpty ?? false) && favicon != null;
|
(title?.isNotEmpty ?? false) && favicon != null;
|
||||||
|
|
||||||
WebPageInfo({required this.url, this.title, this.favicon});
|
WebPageInfo({required this.url, this.title, this.favicon, this.feeds});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get cacheHash => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [url, title, favicon, feeds];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ abstract class _$WebPageInfoCWProxy {
|
|||||||
|
|
||||||
WebPageInfo favicon(BrowserIcon? favicon);
|
WebPageInfo favicon(BrowserIcon? favicon);
|
||||||
|
|
||||||
|
WebPageInfo feeds(Set<String>? feeds);
|
||||||
|
|
||||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebPageInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebPageInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||||
///
|
///
|
||||||
/// Usage
|
/// Usage
|
||||||
@@ -23,6 +25,7 @@ abstract class _$WebPageInfoCWProxy {
|
|||||||
Uri url,
|
Uri url,
|
||||||
String? title,
|
String? title,
|
||||||
BrowserIcon? favicon,
|
BrowserIcon? favicon,
|
||||||
|
Set<String>? feeds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +44,9 @@ class _$WebPageInfoCWProxyImpl implements _$WebPageInfoCWProxy {
|
|||||||
@override
|
@override
|
||||||
WebPageInfo favicon(BrowserIcon? favicon) => this(favicon: favicon);
|
WebPageInfo favicon(BrowserIcon? favicon) => this(favicon: favicon);
|
||||||
|
|
||||||
|
@override
|
||||||
|
WebPageInfo feeds(Set<String>? feeds) => this(feeds: feeds);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
||||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebPageInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebPageInfo(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||||
@@ -53,6 +59,7 @@ class _$WebPageInfoCWProxyImpl implements _$WebPageInfoCWProxy {
|
|||||||
Object? url = const $CopyWithPlaceholder(),
|
Object? url = const $CopyWithPlaceholder(),
|
||||||
Object? title = const $CopyWithPlaceholder(),
|
Object? title = const $CopyWithPlaceholder(),
|
||||||
Object? favicon = const $CopyWithPlaceholder(),
|
Object? favicon = const $CopyWithPlaceholder(),
|
||||||
|
Object? feeds = const $CopyWithPlaceholder(),
|
||||||
}) {
|
}) {
|
||||||
return WebPageInfo(
|
return WebPageInfo(
|
||||||
url: url == const $CopyWithPlaceholder()
|
url: url == const $CopyWithPlaceholder()
|
||||||
@@ -67,6 +74,10 @@ class _$WebPageInfoCWProxyImpl implements _$WebPageInfoCWProxy {
|
|||||||
? _value.favicon
|
? _value.favicon
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: favicon as BrowserIcon?,
|
: favicon as BrowserIcon?,
|
||||||
|
feeds: feeds == const $CopyWithPlaceholder()
|
||||||
|
? _value.feeds
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: feeds as Set<String>?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import 'package:lensai/data/models/web_page_info.dart';
|
|||||||
import 'package:lensai/extensions/nullable.dart';
|
import 'package:lensai/extensions/nullable.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart';
|
import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart';
|
||||||
import 'package:lensai/features/user/domain/repositories/cache.dart';
|
import 'package:lensai/features/user/domain/repositories/cache.dart';
|
||||||
|
import 'package:lensai/presentation/controllers/website_title.dart';
|
||||||
|
import 'package:lensai/utils/feed_finder.dart';
|
||||||
import 'package:lensai/utils/lru_cache.dart';
|
import 'package:lensai/utils/lru_cache.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
@@ -198,10 +200,15 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
|||||||
|
|
||||||
final title = document.querySelector('title')?.text;
|
final title = document.querySelector('title')?.text;
|
||||||
final resources = _extractIcons(baseUri, document);
|
final resources = _extractIcons(baseUri, document);
|
||||||
|
final feeds = await FeedFinder(
|
||||||
|
url: baseUri,
|
||||||
|
document: document,
|
||||||
|
).parse(verifyCandidates: false);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'title': title,
|
'title': title,
|
||||||
'resources': resources.map(_serializeResource).toList(),
|
'resources': resources.map(_serializeResource).toList(),
|
||||||
|
'feeds': feeds,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
@@ -221,6 +228,7 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
|||||||
url: url,
|
url: url,
|
||||||
title: result['title'] as String?,
|
title: result['title'] as String?,
|
||||||
favicon: favicon,
|
favicon: favicon,
|
||||||
|
feeds: result['feeds'] as Set<String>?,
|
||||||
);
|
);
|
||||||
}, exceptionHandler: handleHttpError);
|
}, exceptionHandler: handleHttpError);
|
||||||
}
|
}
|
||||||
@@ -278,9 +286,10 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
|||||||
return cachedIcon;
|
return cachedIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchPageInfo(
|
final result = await ref.read(pageInfoProvider(url).future);
|
||||||
url,
|
final favicon = result.favicon!;
|
||||||
).then((result) => result.flatMap((pageInfo) => pageInfo.favicon!).value);
|
|
||||||
|
return favicon;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<Uri?> tryUpgradeToHttps(Uri httpUri) async {
|
// Future<Uri?> tryUpgradeToHttps(Uri httpUri) async {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'generic_website.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$genericWebsiteServiceHash() =>
|
String _$genericWebsiteServiceHash() =>
|
||||||
r'663759beb3ae7448f4d60339557ba11c1a3a687d';
|
r'12396df7370ebc31b0a53b786e5a3e27c6d05177';
|
||||||
|
|
||||||
/// See also [GenericWebsiteService].
|
/// See also [GenericWebsiteService].
|
||||||
@ProviderFor(GenericWebsiteService)
|
@ProviderFor(GenericWebsiteService)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||||
import 'package:fast_equatable/fast_equatable.dart';
|
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:lensai/data/models/web_page_info.dart';
|
import 'package:lensai/data/models/web_page_info.dart';
|
||||||
import 'package:lensai/domain/entities/equatable_image.dart';
|
import 'package:lensai/domain/entities/equatable_image.dart';
|
||||||
@@ -13,7 +12,7 @@ import 'package:lensai/features/geckoview/domain/entities/states/security.dart';
|
|||||||
part 'tab.g.dart';
|
part 'tab.g.dart';
|
||||||
|
|
||||||
@CopyWith()
|
@CopyWith()
|
||||||
class TabState extends WebPageInfo with FastEquatable {
|
class TabState extends WebPageInfo {
|
||||||
@CopyWithField(immutable: true)
|
@CopyWithField(immutable: true)
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
@@ -85,10 +84,9 @@ class TabState extends WebPageInfo with FastEquatable {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [
|
List<Object?> get hashParameters => [
|
||||||
|
...super.hashParameters,
|
||||||
id,
|
id,
|
||||||
contextId,
|
contextId,
|
||||||
url,
|
|
||||||
title,
|
|
||||||
icon,
|
icon,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
progress,
|
progress,
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
class FeedFinder {
|
||||||
|
final Uri url;
|
||||||
|
final Document document;
|
||||||
|
|
||||||
|
late final String _base;
|
||||||
|
|
||||||
|
FeedFinder({required this.url, required this.document}) {
|
||||||
|
final uri = url.removeFragment();
|
||||||
|
_base = '${uri.scheme}://${uri.authority}';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<String>> _verifyCandidates(Set<String> candidates) async {
|
||||||
|
final results = <String>{};
|
||||||
|
|
||||||
|
final client = http.Client();
|
||||||
|
try {
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
try {
|
||||||
|
await client.get(Uri.parse(candidate));
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.add(candidate);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseBody(Set<String> candidates) {
|
||||||
|
for (final a in document.querySelectorAll('a')) {
|
||||||
|
var href = a.attributes['href'];
|
||||||
|
if (href != null) {
|
||||||
|
if (href.contains('rss') ||
|
||||||
|
href.contains('xml') ||
|
||||||
|
href.contains('feed')) {
|
||||||
|
// Fix relative URLs
|
||||||
|
href = href.startsWith('/') ? _base + href : href;
|
||||||
|
href = href.endsWith('/') ? href.substring(0, href.length - 2) : href;
|
||||||
|
|
||||||
|
// Fix naked URLs
|
||||||
|
href = !href.startsWith('http') ? '$_base/$href' : href;
|
||||||
|
|
||||||
|
candidates.add(href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseHead(Set<String> candidates) {
|
||||||
|
for (final link in document.querySelectorAll("link[rel='alternate']")) {
|
||||||
|
final type = link.attributes['type'];
|
||||||
|
if (type != null) {
|
||||||
|
if (type.contains('rss') || type.contains('xml')) {
|
||||||
|
var href = link.attributes['href'];
|
||||||
|
if (href != null) {
|
||||||
|
// Fix relative URLs
|
||||||
|
href = href.startsWith('/') ? _base + href : href;
|
||||||
|
candidates.add(href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Set<String>> parse({
|
||||||
|
bool parseHead = true,
|
||||||
|
bool parseBody = true,
|
||||||
|
bool verifyCandidates = true,
|
||||||
|
}) async {
|
||||||
|
final candidates = <String>{};
|
||||||
|
|
||||||
|
// Look for feed candidates in head
|
||||||
|
if (parseHead) {
|
||||||
|
_parseHead(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for feed candidates in body
|
||||||
|
if (parseBody) {
|
||||||
|
_parseBody(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify candidates
|
||||||
|
if (verifyCandidates) {
|
||||||
|
return _verifyCandidates(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user