mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-03-03 15:26:47 +00:00
improving link parser
This commit is contained in:
parent
15c5a44b7d
commit
f5d4f97c02
11 changed files with 276 additions and 246 deletions
|
|
@ -8,13 +8,14 @@ import 'package:html/parser.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/html_parser.dart';
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/html.parser.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld_parser.dart';
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/json_ld.parser.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart';
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/mastodon.parser.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/other_parser.dart';
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og.parser.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/twitter_parser.dart';
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/other.parser.dart';
|
||||||
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/twitter.parser.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart';
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/youtube_parser.dart';
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/youtube.parser.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/utils.dart';
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/utils.dart';
|
||||||
|
|
||||||
Future<Metadata?> getMetadata(String link) async {
|
Future<Metadata?> getMetadata(String link) async {
|
||||||
|
|
@ -81,7 +82,7 @@ Future<Metadata?> getInfo(
|
||||||
final document = responseToDocument(response);
|
final document = responseToDocument(response);
|
||||||
if (document == null) return info;
|
if (document == null) return info;
|
||||||
|
|
||||||
final data_ = _parse(document, url: url);
|
final data_ = _parse(document, url);
|
||||||
|
|
||||||
return data_;
|
return data_;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -103,83 +104,44 @@ Document? responseToDocument(http.Response response) {
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
Metadata _parse(Document? document, {String? url}) {
|
Metadata _parse(Document? document, String url) {
|
||||||
final output = Metadata();
|
final output = Metadata()..url = url;
|
||||||
|
|
||||||
final parsers = [
|
final allParsers = [
|
||||||
_openGraph(document),
|
// start with vendor specific to parse the vendor type
|
||||||
_twitterCard(document),
|
MastodonParser(document),
|
||||||
_youtubeCard(document),
|
YoutubeParser(document, url),
|
||||||
_jsonLdSchema(document),
|
TwitterParser(document, url),
|
||||||
_htmlMeta(document),
|
|
||||||
_otherParser(document),
|
JsonLdParser(document),
|
||||||
|
OpenGraphParser(document),
|
||||||
|
HtmlMetaParser(document),
|
||||||
|
OtherParser(document),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (final p in parsers) {
|
for (final parser in allParsers) {
|
||||||
if (p == null) continue;
|
try {
|
||||||
|
output.vendor ??= parser.vendor;
|
||||||
output.title ??= p.title;
|
output.title ??= parser.title;
|
||||||
output.desc ??= p.desc;
|
output.desc ??= parser.desc;
|
||||||
output.image ??= p.image;
|
if (output.vendor == Vendor.twitterPosting) {
|
||||||
output.siteName ??= p.siteName;
|
if (output.image == null) {
|
||||||
output.url ??= p.url ?? url;
|
if (parser.image?.contains('/media/') ?? false) {
|
||||||
|
output.image ??= parser.image;
|
||||||
if (output.hasAllMetadata) break;
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
final url_ = output.url ?? url;
|
output.image ??= parser.image;
|
||||||
final image = output.image;
|
}
|
||||||
if (url_ != null && image != null) {
|
output.siteName ??= parser.siteName;
|
||||||
output.image = Uri.parse(url_).resolve(image).toString();
|
output.publishDate ??= parser.publishDate;
|
||||||
|
output.likeAction ??= parser.likeAction;
|
||||||
|
output.shareAction ??= parser.shareAction;
|
||||||
|
if (output.hasAllMetadata) break;
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
Metadata? _openGraph(Document? document) {
|
|
||||||
try {
|
|
||||||
return OpenGraphParser(document).parse();
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadata? _htmlMeta(Document? document) {
|
|
||||||
try {
|
|
||||||
return HtmlMetaParser(document).parse();
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadata? _jsonLdSchema(Document? document) {
|
|
||||||
try {
|
|
||||||
return JsonLdParser(document).parse();
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadata? _youtubeCard(Document? document) {
|
|
||||||
try {
|
|
||||||
return YoutubeParser(document).parse();
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadata? _twitterCard(Document? document) {
|
|
||||||
try {
|
|
||||||
return TwitterParser(document).parse();
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadata? _otherParser(Document? document) {
|
|
||||||
try {
|
|
||||||
return OtherParser(document).parse();
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
|
enum Vendor { mastodonSocialMediaPosting, youtubeVideo, twitterPosting }
|
||||||
|
|
||||||
mixin BaseMetaInfo {
|
mixin BaseMetaInfo {
|
||||||
|
late String url;
|
||||||
String? title;
|
String? title;
|
||||||
String? desc;
|
String? desc;
|
||||||
String? image;
|
String? image;
|
||||||
String? url;
|
|
||||||
String? siteName;
|
String? siteName;
|
||||||
|
|
||||||
|
Vendor? vendor;
|
||||||
|
|
||||||
|
DateTime? publishDate;
|
||||||
|
int? likeAction; // https://schema.org/LikeAction
|
||||||
|
int? shareAction; // https://schema.org/ShareAction
|
||||||
|
|
||||||
/// Returns `true` if any parameter other than [url] is filled.
|
/// Returns `true` if any parameter other than [url] is filled.
|
||||||
bool get hasData =>
|
bool get hasData =>
|
||||||
((title?.isNotEmpty ?? false) && title != 'null') ||
|
((title?.isNotEmpty ?? false) && title != 'null') ||
|
||||||
((desc?.isNotEmpty ?? false) && desc != 'null') ||
|
((desc?.isNotEmpty ?? false) && desc != 'null') ||
|
||||||
((image?.isNotEmpty ?? false) && image != 'null');
|
((image?.isNotEmpty ?? false) && image != 'null');
|
||||||
|
|
||||||
Metadata parse() {
|
|
||||||
return Metadata()
|
|
||||||
..title = title
|
|
||||||
..desc = desc
|
|
||||||
..image = image
|
|
||||||
..url = url
|
|
||||||
..siteName = siteName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Container class for Metadata.
|
/// Container class for Metadata.
|
||||||
class Metadata with BaseMetaInfo {
|
class Metadata with BaseMetaInfo {
|
||||||
|
Metadata();
|
||||||
bool get hasAllMetadata {
|
bool get hasAllMetadata {
|
||||||
return title != null && desc != null && image != null && url != null;
|
return title != null && desc != null && image != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,4 @@ class HtmlMetaParser with BaseMetaInfo {
|
||||||
?.querySelector("meta[name='site_name']")
|
?.querySelector("meta[name='site_name']")
|
||||||
?.attributes
|
?.attributes
|
||||||
.get('content');
|
.get('content');
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => parse().toString();
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||||
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og.parser.dart';
|
||||||
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart';
|
||||||
|
|
||||||
|
/// Parses [Metadata] from `json-ld` data in `<script>` tags.
|
||||||
|
class JsonLdParser with BaseMetaInfo {
|
||||||
|
JsonLdParser(this.document) {
|
||||||
|
_parseToJson(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
Document? document;
|
||||||
|
Map<String, dynamic>? _jsonData;
|
||||||
|
|
||||||
|
void _parseToJson(Document? document) {
|
||||||
|
try {
|
||||||
|
final data = document?.head
|
||||||
|
?.querySelector("script[type='application/ld+json']")
|
||||||
|
?.innerHtml;
|
||||||
|
if (data == null) return;
|
||||||
|
// For multiline json file
|
||||||
|
// Replacing all new line characters with empty space
|
||||||
|
// before performing json decode on data
|
||||||
|
_jsonData =
|
||||||
|
jsonDecode(data.replaceAll('\n', ' ')) as Map<String, dynamic>;
|
||||||
|
// ignore: empty_catches
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the [Metadata.title] from the <title> tag.
|
||||||
|
@override
|
||||||
|
String? get title {
|
||||||
|
final data = _jsonData;
|
||||||
|
if (data is Map<String, dynamic>) {
|
||||||
|
return data['name'] as String? ?? data['headline'] as String?;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int? get shareAction {
|
||||||
|
final statistics = _jsonData?['interactionStatistic'] as List<dynamic>?;
|
||||||
|
if (statistics != null) {
|
||||||
|
for (final statsDy in statistics) {
|
||||||
|
final stats = statsDy as Map<String, dynamic>?;
|
||||||
|
if (stats != null) {
|
||||||
|
if (stats['interactionType'] == 'https://schema.org/ShareAction') {
|
||||||
|
return stats['userInteractionCount'] as int?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int? get likeAction {
|
||||||
|
final statistics = _jsonData?['interactionStatistic'] as List<dynamic>?;
|
||||||
|
if (statistics != null) {
|
||||||
|
for (final statsDy in statistics) {
|
||||||
|
final stats = statsDy as Map<String, dynamic>?;
|
||||||
|
if (stats != null) {
|
||||||
|
if (stats['interactionType'] == 'https://schema.org/LikeAction') {
|
||||||
|
return stats['userInteractionCount'] as int?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get desc {
|
||||||
|
return _jsonData?['description'] as String?;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the [Metadata.image] from the first <img> tag in the body.
|
||||||
|
@override
|
||||||
|
String? get image {
|
||||||
|
final data = _jsonData;
|
||||||
|
return _imgResultToStr(
|
||||||
|
data?.getDynamic('logo') ?? data?.getDynamic('image'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JSON LD does not have a siteName property, so we get it from
|
||||||
|
/// [og:site_name] if available.
|
||||||
|
@override
|
||||||
|
String? get siteName => OpenGraphParser(document).siteName;
|
||||||
|
|
||||||
|
String? _imgResultToStr(dynamic result) {
|
||||||
|
if (result is List && result.isNotEmpty) result = result.first;
|
||||||
|
if (result is String) return result;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:html/dom.dart';
|
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/og_parser.dart';
|
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart';
|
|
||||||
|
|
||||||
/// Parses [Metadata] from `json-ld` data in `<script>` tags.
|
|
||||||
class JsonLdParser with BaseMetaInfo {
|
|
||||||
JsonLdParser(this.document) {
|
|
||||||
_jsonData = _parseToJson(document);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The [Document] to parse.
|
|
||||||
Document? document;
|
|
||||||
dynamic _jsonData;
|
|
||||||
|
|
||||||
dynamic _parseToJson(Document? document) {
|
|
||||||
final data = document?.head
|
|
||||||
?.querySelector("script[type='application/ld+json']")
|
|
||||||
?.innerHtml;
|
|
||||||
if (data == null) return null;
|
|
||||||
// For multiline json file
|
|
||||||
// Replacing all new line characters with empty space
|
|
||||||
// before performing json decode on data
|
|
||||||
return jsonDecode(data.replaceAll('\n', ' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the [Metadata.title] from the <title> tag.
|
|
||||||
@override
|
|
||||||
String? get title {
|
|
||||||
final data = _jsonData;
|
|
||||||
if (data is Map<String, dynamic>) {
|
|
||||||
return data['name'] as String? ?? data['headline'] as String?;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the [Metadata.desc] from the content of the
|
|
||||||
/// <meta name="description"> tag.
|
|
||||||
@override
|
|
||||||
String? get desc {
|
|
||||||
final data = _jsonData;
|
|
||||||
if (data is List<Map<String, dynamic>>) {
|
|
||||||
return data.first['description'] as String? ??
|
|
||||||
data.first['headline'] as String?;
|
|
||||||
} else if (data is Map<String, dynamic>) {
|
|
||||||
return data['description'] as String? ?? data['description'] as String?;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the [Metadata.image] from the first <img> tag in the body.
|
|
||||||
@override
|
|
||||||
String? get image {
|
|
||||||
final data = _jsonData;
|
|
||||||
if (data is List && data.isNotEmpty) {
|
|
||||||
return _imgResultToStr(data.first['logo'] ?? data.first['image']);
|
|
||||||
} else if (data is Map) {
|
|
||||||
return _imgResultToStr(
|
|
||||||
data.getDynamic('logo') ?? data.getDynamic('image'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// JSON LD does not have a siteName property, so we get it from
|
|
||||||
/// [og:site_name] if available.
|
|
||||||
@override
|
|
||||||
String? get siteName => OpenGraphParser(document).siteName;
|
|
||||||
|
|
||||||
String? _imgResultToStr(dynamic result) {
|
|
||||||
if (result is List && result.isNotEmpty) result = result.first;
|
|
||||||
if (result is String) return result;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => parse().toString();
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||||
|
|
||||||
|
class MastodonParser with BaseMetaInfo {
|
||||||
|
MastodonParser(this._document);
|
||||||
|
final Document? _document;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Vendor? get vendor => ((_document?.head?.innerHtml
|
||||||
|
.contains('"repository":"mastodon/mastodon"') ??
|
||||||
|
false) &&
|
||||||
|
(_document?.head?.innerHtml.contains('SocialMediaPosting') ?? false))
|
||||||
|
? Vendor.mastodonSocialMediaPosting
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
@ -25,11 +25,4 @@ class OpenGraphParser with BaseMetaInfo {
|
||||||
/// Get [Metadata.siteName] from 'og:site_name'.
|
/// Get [Metadata.siteName] from 'og:site_name'.
|
||||||
@override
|
@override
|
||||||
String? get siteName => getProperty(_document, property: 'og:site_name');
|
String? get siteName => getProperty(_document, property: 'og:site_name');
|
||||||
|
|
||||||
/// Get [Metadata.url] from 'og:url'.
|
|
||||||
@override
|
|
||||||
String? get url => getProperty(_document, property: 'og:url');
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => parse().toString();
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,33 +7,21 @@ import 'util.dart';
|
||||||
class OtherParser with BaseMetaInfo {
|
class OtherParser with BaseMetaInfo {
|
||||||
OtherParser(this._document);
|
OtherParser(this._document);
|
||||||
|
|
||||||
/// The [Document] to be parse
|
|
||||||
final Document? _document;
|
final Document? _document;
|
||||||
|
|
||||||
/// Get [Metadata.title] from 'title'.
|
|
||||||
@override
|
@override
|
||||||
String? get title =>
|
String? get title =>
|
||||||
getProperty(_document, attribute: 'name', property: 'title');
|
getProperty(_document, attribute: 'name', property: 'title');
|
||||||
|
|
||||||
/// Get [Metadata.desc] from 'description'.
|
|
||||||
@override
|
@override
|
||||||
String? get desc =>
|
String? get desc =>
|
||||||
getProperty(_document, attribute: 'name', property: 'description');
|
getProperty(_document, attribute: 'name', property: 'description');
|
||||||
|
|
||||||
/// Get [Metadata.image] from 'image'.
|
|
||||||
@override
|
@override
|
||||||
String? get image =>
|
String? get image =>
|
||||||
getProperty(_document, attribute: 'name', property: 'image');
|
getProperty(_document, attribute: 'name', property: 'image');
|
||||||
|
|
||||||
/// Get [Metadata.siteName] from 'description'.
|
|
||||||
@override
|
@override
|
||||||
String? get siteName =>
|
String? get siteName =>
|
||||||
getProperty(_document, attribute: 'name', property: 'site_name');
|
getProperty(_document, attribute: 'name', property: 'site_name');
|
||||||
|
|
||||||
/// Get [Metadata.url] from 'url'.
|
|
||||||
@override
|
|
||||||
String? get url => getProperty(_document, attribute: 'name', property: 'url');
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => parse().toString();
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||||
import 'base.dart';
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/util.dart';
|
||||||
import 'og_parser.dart';
|
|
||||||
import 'util.dart';
|
|
||||||
|
|
||||||
/// Parses [Metadata] from `<meta property='twitter:*'>` tags.
|
/// Parses [Metadata] from `<meta property='twitter:*'>` tags.
|
||||||
class TwitterParser with BaseMetaInfo {
|
class TwitterParser with BaseMetaInfo {
|
||||||
TwitterParser(this._document);
|
TwitterParser(this._document, this._url);
|
||||||
|
|
||||||
/// The [Document] to parse.
|
|
||||||
final Document? _document;
|
final Document? _document;
|
||||||
|
final String _url;
|
||||||
|
|
||||||
/// Get [Metadata.title] from 'twitter:title'
|
|
||||||
@override
|
@override
|
||||||
String? get title =>
|
String? get title =>
|
||||||
getProperty(_document, attribute: 'name', property: 'twitter:title') ??
|
getProperty(_document, attribute: 'name', property: 'twitter:title') ??
|
||||||
getProperty(_document, property: 'twitter:title');
|
getProperty(_document, property: 'twitter:title');
|
||||||
|
|
||||||
/// Get [Metadata.desc] from 'twitter:description'
|
|
||||||
@override
|
@override
|
||||||
String? get desc =>
|
String? get desc =>
|
||||||
getProperty(
|
getProperty(
|
||||||
|
|
@ -27,22 +22,14 @@ class TwitterParser with BaseMetaInfo {
|
||||||
) ??
|
) ??
|
||||||
getProperty(_document, property: 'twitter:description');
|
getProperty(_document, property: 'twitter:description');
|
||||||
|
|
||||||
/// Get [Metadata.image] from 'twitter:image'
|
|
||||||
@override
|
@override
|
||||||
String? get image =>
|
String? get image =>
|
||||||
getProperty(_document, attribute: 'name', property: 'twitter:image') ??
|
getProperty(_document, attribute: 'name', property: 'twitter:image') ??
|
||||||
getProperty(_document, property: 'twitter:image');
|
getProperty(_document, property: 'twitter:image');
|
||||||
|
|
||||||
/// Twitter Cards do not have a siteName property, so we get it from
|
|
||||||
/// [og:site_name] if available.
|
|
||||||
@override
|
@override
|
||||||
String? get siteName => OpenGraphParser(_document).siteName;
|
Vendor? get vendor =>
|
||||||
|
_url.startsWith('https://x.com/') && _url.contains('/status/')
|
||||||
/// Twitter Cards do not have a url property, so we get the url from
|
? Vendor.twitterPosting
|
||||||
/// [og:url] if available.
|
: null;
|
||||||
@override
|
|
||||||
String? get url => OpenGraphParser(_document).url;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => parse().toString();
|
|
||||||
}
|
}
|
||||||
|
|
@ -4,26 +4,32 @@ import 'base.dart';
|
||||||
import 'util.dart';
|
import 'util.dart';
|
||||||
|
|
||||||
class YoutubeParser with BaseMetaInfo {
|
class YoutubeParser with BaseMetaInfo {
|
||||||
YoutubeParser(this.document) {
|
YoutubeParser(this.document, this.url) {
|
||||||
_jsonData = _parseToJson(document);
|
_jsonData = _parseToJson(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String url;
|
||||||
|
|
||||||
Document? document;
|
Document? document;
|
||||||
dynamic _jsonData;
|
dynamic _jsonData;
|
||||||
|
|
||||||
dynamic _parseToJson(Document? document) {
|
dynamic _parseToJson(Document? document) {
|
||||||
final data = document?.outerHtml
|
try {
|
||||||
.replaceAll('<html><head></head><body>', '')
|
final data = document?.outerHtml
|
||||||
.replaceAll('</body></html>', '');
|
.replaceAll('<html><head></head><body>', '')
|
||||||
if (data == null) return null;
|
.replaceAll('</body></html>', '');
|
||||||
/* For multiline json file */
|
if (data == null) return null;
|
||||||
// Replacing all new line characters with empty space
|
/* For multiline json file */
|
||||||
// before performing json decode on data
|
// Replacing all new line characters with empty space
|
||||||
final d = jsonDecode(data.replaceAll('\n', ' '));
|
// before performing json decode on data
|
||||||
return d;
|
final d = jsonDecode(data.replaceAll('\n', ' '));
|
||||||
|
return d;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the [Metadata.title] from the [<title>] tag
|
|
||||||
@override
|
@override
|
||||||
String? get title {
|
String? get title {
|
||||||
final data = _jsonData;
|
final data = _jsonData;
|
||||||
|
|
@ -35,7 +41,6 @@ class YoutubeParser with BaseMetaInfo {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the [Metadata.image] from the first <img> tag in the body
|
|
||||||
@override
|
@override
|
||||||
String? get image {
|
String? get image {
|
||||||
final data = _jsonData;
|
final data = _jsonData;
|
||||||
|
|
@ -59,22 +64,13 @@ class YoutubeParser with BaseMetaInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get url {
|
Vendor? get vendor => (Uri.parse(url).host.contains('youtube.com'))
|
||||||
final data = _jsonData;
|
? Vendor.youtubeVideo
|
||||||
if (data is List<Map<String, dynamic>>) {
|
: null;
|
||||||
return data.first['provider_url'] as String?;
|
|
||||||
} else if (data is Map) {
|
|
||||||
return data.get('provider_url');
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _imgResultToStr(dynamic result) {
|
String? _imgResultToStr(dynamic result) {
|
||||||
if (result is List && result.isNotEmpty) result = result.first;
|
if (result is List && result.isNotEmpty) result = result.first;
|
||||||
if (result is String) return result;
|
if (result is String) return result;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => parse().toString();
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,20 +1,30 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart';
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart';
|
||||||
|
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||||
|
|
||||||
class LinkParserTest {
|
class LinkParserTest {
|
||||||
LinkParserTest({
|
LinkParserTest({
|
||||||
required this.url,
|
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.siteName,
|
required this.url,
|
||||||
required this.desc,
|
this.desc,
|
||||||
this.image,
|
this.image,
|
||||||
|
this.siteName,
|
||||||
|
this.vendor,
|
||||||
|
this.publishDate,
|
||||||
|
this.likeAction,
|
||||||
|
this.shareAction,
|
||||||
});
|
});
|
||||||
|
String title;
|
||||||
|
String? desc;
|
||||||
|
String? image;
|
||||||
|
String url;
|
||||||
|
String? siteName;
|
||||||
|
|
||||||
final String url;
|
Vendor? vendor;
|
||||||
final String title;
|
|
||||||
final String siteName;
|
DateTime? publishDate;
|
||||||
final String desc;
|
int? likeAction; // https://schema.org/LikeAction
|
||||||
final String? image;
|
int? shareAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
@ -27,6 +37,9 @@ void main() {
|
||||||
desc: 'Attached: 1 image',
|
desc: 'Attached: 1 image',
|
||||||
image:
|
image:
|
||||||
'https://files.mastodon.social/media_attachments/files/115/883/317/526/523/824/original/6fa7ef90ec68f1f1.jpg',
|
'https://files.mastodon.social/media_attachments/files/115/883/317/526/523/824/original/6fa7ef90ec68f1f1.jpg',
|
||||||
|
vendor: Vendor.mastodonSocialMediaPosting,
|
||||||
|
shareAction: 90,
|
||||||
|
likeAction: 290,
|
||||||
),
|
),
|
||||||
LinkParserTest(
|
LinkParserTest(
|
||||||
url: 'https://chaos.social/@netzpolitik_feed/115921534467938262',
|
url: 'https://chaos.social/@netzpolitik_feed/115921534467938262',
|
||||||
|
|
@ -36,7 +49,53 @@ void main() {
|
||||||
'Die EU-Kommission erkennt Open Source als entscheidend für die digitale Souveränität an und wünscht sich mehr Kommerzialisierung. Bis April will Brüssel eine neue Strategie veröffentlichen. In einer laufenden Konsultation bekräftigen Stimmen aus ganz Europa, welche Vorteile sie in offenem Quellcode sehen.\n'
|
'Die EU-Kommission erkennt Open Source als entscheidend für die digitale Souveränität an und wünscht sich mehr Kommerzialisierung. Bis April will Brüssel eine neue Strategie veröffentlichen. In einer laufenden Konsultation bekräftigen Stimmen aus ganz Europa, welche Vorteile sie in offenem Quellcode sehen.\n'
|
||||||
'\n'
|
'\n'
|
||||||
'https://netzpolitik.org/2026/konsultation-eu-kommission-arbeitet-an-neuer-open-source-strategie/',
|
'https://netzpolitik.org/2026/konsultation-eu-kommission-arbeitet-an-neuer-open-source-strategie/',
|
||||||
|
vendor: Vendor.mastodonSocialMediaPosting,
|
||||||
|
shareAction: 70,
|
||||||
|
likeAction: 90,
|
||||||
),
|
),
|
||||||
|
LinkParserTest(
|
||||||
|
title: 'Kuketz-Blog 🛡 (@kuketzblog@social.tchncs.de)',
|
||||||
|
url: 'https://social.tchncs.de/@kuketzblog/115898752560771936',
|
||||||
|
siteName: 'Mastodon',
|
||||||
|
desc:
|
||||||
|
'AWS verspricht jetzt »Souveränität« mit einem »europäischen« Cloud-Angebot – Standort Deutschland, großes Vertrauens-Theater.\n'
|
||||||
|
'\n'
|
||||||
|
'Nur: Souveränität ist keine Postleitzahl. Wenn der Anbieter Amazon heißt, bleibt es dasselbe Märchen mit neuem Umschlag: Der Cloud Act, FISA etc. gilt trotzdem. US-Recht schlägt Geografie. Das Gerede von »Souveränität« ist kein Konzept, sondern Marketing.\n'
|
||||||
|
'\n'
|
||||||
|
'https://www.heise.de/news/AWS-verspricht-Souveraenitaet-mit-europaeischem-Cloudangebot-11141800.html',
|
||||||
|
vendor: Vendor.mastodonSocialMediaPosting,
|
||||||
|
shareAction: 15,
|
||||||
|
likeAction: 190,
|
||||||
|
),
|
||||||
|
LinkParserTest(
|
||||||
|
title:
|
||||||
|
'David Kriesel: Traue keinem Scan, den du nicht selbst gefälscht hast',
|
||||||
|
url: 'https://www.youtube.com/watch?v=7FeqF1-Z1g0',
|
||||||
|
siteName: 'YouTube',
|
||||||
|
vendor: Vendor.youtubeVideo,
|
||||||
|
image: 'https://i.ytimg.com/vi/7FeqF1-Z1g0/hqdefault.jpg',
|
||||||
|
),
|
||||||
|
LinkParserTest(
|
||||||
|
title: 'netzpolitik.org (@netzpolitik_org) on X',
|
||||||
|
url: 'https://x.com/netzpolitik_org/status/1782791019412529665',
|
||||||
|
siteName: 'X (formerly Twitter)',
|
||||||
|
desc:
|
||||||
|
'Jetzt ist wirklich Schluss: Wir verlassen als Redaktion das zur Plattform für Rechtsradikale verkommene Twitter – und freuen uns, wenn ihr uns woanders folgt.\n'
|
||||||
|
'\n'
|
||||||
|
'https://t.co/8W0hGly5bL',
|
||||||
|
vendor: Vendor.twitterPosting,
|
||||||
|
),
|
||||||
|
LinkParserTest(
|
||||||
|
title: 'netzpolitik.org (@netzpolitik_org) on X',
|
||||||
|
url: 'https://x.com/netzpolitik_org/status/1162346968124968960',
|
||||||
|
siteName: 'X (formerly Twitter)',
|
||||||
|
desc:
|
||||||
|
'Weil unsere Datenanalyse zum Twitter-Account von Maaßen rechte Millieus und ihre Verbindungen offengelegt hat, haben wir einen rechten Shitstorm an der Backe. Klar ist: Wir lassen uns nicht einschüchtern und freuen uns auf Unterstützung! \n'
|
||||||
|
'\n'
|
||||||
|
'https://t.co/MQZ7ulHakF',
|
||||||
|
image: 'https://pbs.twimg.com/media/ECF8Z5KWwAIBZ6o.jpg:large',
|
||||||
|
vendor: Vendor.twitterPosting,
|
||||||
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
for (final testCase in testCases) {
|
for (final testCase in testCases) {
|
||||||
|
|
@ -44,7 +103,22 @@ void main() {
|
||||||
expect(metadata.title, testCase.title);
|
expect(metadata.title, testCase.title);
|
||||||
expect(metadata.siteName, testCase.siteName);
|
expect(metadata.siteName, testCase.siteName);
|
||||||
expect(metadata.desc, testCase.desc);
|
expect(metadata.desc, testCase.desc);
|
||||||
|
expect(metadata.url, testCase.url);
|
||||||
expect(metadata.image, testCase.image);
|
expect(metadata.image, testCase.image);
|
||||||
|
expect(metadata.vendor, testCase.vendor, reason: metadata.url);
|
||||||
|
if (testCase.shareAction != null) {
|
||||||
|
expect(
|
||||||
|
metadata.shareAction,
|
||||||
|
greaterThanOrEqualTo(testCase.shareAction!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (testCase.shareAction != null) {
|
||||||
|
expect(
|
||||||
|
metadata.likeAction,
|
||||||
|
greaterThanOrEqualTo(testCase.likeAction!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(metadata.publishDate, testCase.publishDate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue