import 'package:collection/collection.dart'; import 'package:ndk/ndk.dart'; /// Container class over event and stream info class StreamEvent { late final StreamInfo info; final Nip01Event event; /// Return the 'a' tag for this stream String get aTag { return "${event.kind}:${event.pubKey}:${info.id}"; } StreamEvent(this.event) { info = extractStreamInfo(event); } } enum StreamStatus { live, ended, planned } /// Extracted tags from NIP-53 event class StreamInfo { String? id; String? title; String? summary; String? image; String? thumbnail; StreamStatus? status; String? stream; String? recording; String? contentWarning; List tags; String? goal; int? participants; int? starts; int? ends; String? service; String host; String? gameId; GameInfo? gameInfo; List streams; StreamInfo({ this.id, this.title, this.summary, this.image, this.thumbnail, this.status, this.stream, this.recording, this.contentWarning, this.tags = const [], this.goal, this.participants, this.starts, this.ends, this.service, required this.host, this.gameId, this.gameInfo, this.streams = const [], }); } class GameInfo { String id; String name; List genres; String className; GameInfo({ required this.id, required this.name, required this.genres, required this.className, }); } final RegExp gameTagFormat = RegExp( r'^[a-z-]+:[a-z0-9-]+$', caseSensitive: false, ); StreamInfo extractStreamInfo(Nip01Event ev) { var ret = StreamInfo(host: getHost(ev)); void matchTag(List tag, String k, void Function(String) into) { if (tag[0] == k) { into(tag[1]); } } for (var t in ev.tags) { matchTag(t, 'd', (v) => ret.id = v); matchTag(t, 'title', (v) => ret.title = v); matchTag(t, 'summary', (v) => ret.summary = v); matchTag(t, 'image', (v) => ret.image = v); matchTag(t, 'thumbnail', (v) => ret.thumbnail = v); matchTag( t, 'status', (v) => ret.status = switch (v.toLowerCase()) { 'live' => StreamStatus.live, 'ended' => StreamStatus.ended, 'planned' => StreamStatus.planned, _ => null, }, ); if (t[0] == 'streaming') { ret.streams = [...ret.streams, t[1]]; } matchTag(t, 'recording', (v) => ret.recording = v); matchTag(t, 'url', (v) => ret.recording = v); matchTag(t, 'content-warning', (v) => ret.contentWarning = v); matchTag(t, 'current_participants', (v) => ret.participants = int.tryParse(v)); matchTag(t, 'goal', (v) => ret.goal = v); matchTag(t, 'starts', (v) => ret.starts = int.tryParse(v)); matchTag(t, 'ends', (v) => ret.ends = int.tryParse(v)); matchTag(t, 'service', (v) => ret.service = v); } var sortedTags = sortStreamTags(ev.tags); ret.tags = sortedTags.regularTags; var gameTag = extractGameTag(sortedTags.prefixedTags); ret.gameId = gameTag.gameId; ret.gameInfo = gameTag.gameInfo; if (ret.streams.isNotEmpty) { var isN94 = ret.streams.contains('nip94'); if (isN94) { ret.stream = 'nip94'; } else { ret.stream = ret.streams.firstWhereOrNull((a) => a.contains('.m3u8')); } } return ret; } ({List regularTags, List prefixedTags}) sortStreamTags( List tags, ) { var plainTags = tags .where((a) => a is List ? a[0] == 't' : true) .map((a) => a is List ? a[1] : a as String) .toList(); var regularTags = plainTags.where((a) => !gameTagFormat.hasMatch(a)).toList(); var prefixedTags = plainTags.where((a) => !regularTags.contains(a)).toList(); return (regularTags: regularTags, prefixedTags: prefixedTags); } ({GameInfo? gameInfo, String? gameId}) extractGameTag(List tags) { final gameId = tags.firstWhereOrNull((a) => gameTagFormat.hasMatch(a)); final internalGame = AllCategories.firstWhereOrNull( (a) => gameId == 'internal:${a.id}', ); if (internalGame != null) { return ( gameInfo: GameInfo( id: internalGame.id, name: internalGame.name, genres: internalGame.tags, className: internalGame.className, ), gameId: gameId, ); } final lowerTags = tags.map((t) => t.toLowerCase()); final taggedCategory = AllCategories.firstWhereOrNull( (a) => a.tags.any(lowerTags.contains), ); if (taggedCategory != null) { return ( gameInfo: GameInfo( id: taggedCategory.id, name: taggedCategory.name, genres: taggedCategory.tags, className: taggedCategory.className, ), gameId: gameId, ); } return (gameInfo: null, gameId: gameId); } String getHost(Nip01Event ev) { return ev.tags.firstWhere( (t) => t[0] == "p" && t[3] == "host", orElse: () => ["p", ev.pubKey], // fake p tag with event pubkey )[1]; } class Category { String id; String name; List tags; String className; Category({ required this.id, required this.name, required this.tags, required this.className, }); } List AllCategories = []; // Implement as needed