Files
zap-stream-flutter/lib/utils.dart
2025-05-08 16:30:30 +01:00

216 lines
5.1 KiB
Dart

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<String> tags;
String? goal;
int? participants;
int? starts;
int? ends;
String? service;
String host;
String? gameId;
GameInfo? gameInfo;
List<String> 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<String> 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<String> 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<String> regularTags, List<String> prefixedTags}) sortStreamTags(
List<dynamic> tags,
) {
var plainTags =
tags
.where((a) => a is List<String> ? a[0] == 't' : true)
.map((a) => a is List<String> ? 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<String> 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<String> tags;
String className;
Category({
required this.id,
required this.name,
required this.tags,
required this.className,
});
}
List<Category> AllCategories = []; // Implement as needed