Migrate from BTTV's FFZ API to FFZ's official API

This commit is contained in:
Tommy Chow 2021-12-31 20:34:12 -05:00
parent 5d321b7c00
commit e768a05c37
4 changed files with 139 additions and 102 deletions

View file

@ -3,16 +3,23 @@ import 'package:flutter/foundation.dart';
import 'package:frosty/models/badges.dart';
import 'package:frosty/models/emotes.dart';
import 'package:http/http.dart' as http;
import 'package:tuple/tuple.dart';
class FFZ {
/// Returns a map of global FFZ emotes to their URL.
static Future<List<Emote>> getEmotesGlobal() async {
final url = Uri.parse('https://api.betterttv.net/3/cached/frankerfacez/emotes/global');
final url = Uri.parse('https://api.frankerfacez.com/v1/set/global');
final response = await http.get(url);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body) as List;
final emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote));
final decoded = jsonDecode(response.body);
final defaultSets = decoded['default_sets'] as List;
final emotes = <EmoteFFZ>[];
for (final setId in defaultSets) {
final emoticons = decoded['sets'][setId.toString()]['emoticons'] as List;
emotes.addAll(emoticons.map((emote) => EmoteFFZ.fromJson(emote)));
}
return emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzGlobal)).toList();
} else {
@ -21,60 +28,44 @@ class FFZ {
}
}
/// Returns a map of a channel's FFZ emotes to their URL.
static Future<List<Emote>> getEmotesChannel({required String id}) async {
final url = Uri.parse('https://api.betterttv.net/3/cached/frankerfacez/users/twitch/$id');
final response = await http.get(url);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body) as List;
final emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote));
return emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzChannel)).toList();
} else {
debugPrint('Failed to get FFZ emotes for id: $id. Error code: ${response.statusCode}');
return [];
}
}
static Future<Map<String, List<BadgeInfoFFZ>>?> getBadges() async {
final url = Uri.parse('https://api.frankerfacez.com/v1/badges');
/// Returns a channel's FFZ room info including custom badges and emote used.
static Future<Tuple2<RoomFFZ, List<Emote>>?> getRoomInfo({required String name}) async {
final url = Uri.parse('https://api.frankerfacez.com/v1/room/$name');
final response = await http.get(url);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
final badges = decoded['badges'] as List;
final users = decoded['users'];
final roomInfo = RoomFFZ.fromJson(decoded['room']);
final emoticons = decoded['sets'][roomInfo.set.toString()]['emoticons'] as List;
final emotes = emoticons.map((emote) => EmoteFFZ.fromJson(emote));
return Tuple2(roomInfo, emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzChannel)).toList());
} else {
debugPrint('Failed to get FFZ emotes for id: $name. Error code: ${response.statusCode}');
}
}
static Future<Map<String, List<BadgeInfoFFZ>>?> getBadges() async {
final url = Uri.parse('https://api.frankerfacez.com/v1/badges/ids');
final response = await http.get(url);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
final badges = decoded['badges'] as List;
final badgeObjects = badges.map((badge) => BadgeInfoFFZ.fromJson(badge)).toList();
final result = <String, List<BadgeInfoFFZ>>{};
final developers = users['1'] as List;
final bots = users['2'] as List;
final supporters = users['3'] as List;
for (var username in developers) {
final entry = result[username];
if (entry != null) {
entry.add(badgeObjects[0]);
} else {
result[username] = [badgeObjects[0]];
}
}
for (var username in bots) {
final entry = result[username];
if (entry != null) {
entry.add(badgeObjects[1]);
} else {
result[username] = [badgeObjects[1]];
}
}
for (var username in supporters) {
final entry = result[username];
if (entry != null) {
entry.add(badgeObjects[2]);
} else {
result[username] = [badgeObjects[2]];
for (final badge in badgeObjects.reversed) {
for (final userId in decoded['users'][badge.id.toString()]) {
final entry = result[userId.toString()];
if (entry == null) {
result[userId.toString()] = [badge];
} else {
entry.add(badge);
}
}
}

View file

@ -106,28 +106,32 @@ class EmoteBTTVChannel {
}
// * FFZ Emotes *
@JsonSerializable(createToJson: false)
class UserFFZ {
final int id;
final String name;
final String displayName;
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
class RoomFFZ {
final int set;
final String? moderatorBadge;
final ImagesFFZ? vipBadge;
final ImagesFFZ? modUrls;
// final Map<String, List<String>> userBadges;
// final Map<String, List<int>> userBadgesIds;
const UserFFZ(
this.id,
this.name,
this.displayName,
const RoomFFZ(
this.set,
this.moderatorBadge,
this.vipBadge,
this.modUrls,
);
factory UserFFZ.fromJson(Map<String, dynamic> json) => _$UserFFZFromJson(json);
factory RoomFFZ.fromJson(Map<String, dynamic> json) => _$RoomFFZFromJson(json);
}
@JsonSerializable(createToJson: false)
class ImagesFFZ {
@JsonKey(name: '1x')
@JsonKey(name: '1')
final String url1x;
@JsonKey(name: '2x')
@JsonKey(name: '2')
final String? url2x;
@JsonKey(name: '4x')
@JsonKey(name: '4')
final String? url4x;
const ImagesFFZ(
@ -142,22 +146,40 @@ class ImagesFFZ {
@JsonSerializable(createToJson: false)
class EmoteFFZ {
final int id;
final UserFFZ user;
final String code;
final ImagesFFZ images;
final String imageType;
final String name;
final int height;
final int width;
final OwnerFFZ owner;
final ImagesFFZ urls;
const EmoteFFZ(
this.id,
this.user,
this.code,
this.images,
this.imageType,
this.name,
this.height,
this.width,
this.owner,
this.urls,
);
factory EmoteFFZ.fromJson(Map<String, dynamic> json) => _$EmoteFFZFromJson(json);
}
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
class OwnerFFZ {
@JsonKey(name: '_id')
final int id;
final String name;
final String displayName;
const OwnerFFZ(
this.id,
this.name,
this.displayName,
);
factory OwnerFFZ.fromJson(Map<String, dynamic> json) => _$OwnerFFZFromJson(json);
}
// * 7TV Emotes *
@JsonSerializable(createToJson: false)
@ -272,9 +294,9 @@ class Emote {
factory Emote.fromFFZ(EmoteFFZ emote, EmoteType type) => Emote(
id: emote.id.toString(),
name: emote.code,
name: emote.name,
zeroWidth: false,
url: emote.images.url4x ?? emote.images.url2x ?? emote.images.url1x,
url: 'https:' + (emote.urls.url4x ?? emote.urls.url2x ?? emote.urls.url1x),
type: type,
);

View file

@ -54,24 +54,36 @@ EmoteBTTVChannel _$EmoteBTTVChannelFromJson(Map<String, dynamic> json) =>
.toList(),
);
UserFFZ _$UserFFZFromJson(Map<String, dynamic> json) => UserFFZ(
json['id'] as int,
json['name'] as String,
json['displayName'] as String,
RoomFFZ _$RoomFFZFromJson(Map<String, dynamic> json) => RoomFFZ(
json['set'] as int,
json['moderator_badge'] as String?,
json['vip_badge'] == null
? null
: ImagesFFZ.fromJson(json['vip_badge'] as Map<String, dynamic>),
json['mod_urls'] == null
? null
: ImagesFFZ.fromJson(json['mod_urls'] as Map<String, dynamic>),
);
ImagesFFZ _$ImagesFFZFromJson(Map<String, dynamic> json) => ImagesFFZ(
json['1x'] as String,
json['2x'] as String?,
json['4x'] as String?,
json['1'] as String,
json['2'] as String?,
json['4'] as String?,
);
EmoteFFZ _$EmoteFFZFromJson(Map<String, dynamic> json) => EmoteFFZ(
json['id'] as int,
UserFFZ.fromJson(json['user'] as Map<String, dynamic>),
json['code'] as String,
ImagesFFZ.fromJson(json['images'] as Map<String, dynamic>),
json['imageType'] as String,
json['name'] as String,
json['height'] as int,
json['width'] as int,
OwnerFFZ.fromJson(json['owner'] as Map<String, dynamic>),
ImagesFFZ.fromJson(json['urls'] as Map<String, dynamic>),
);
OwnerFFZ _$OwnerFFZFromJson(Map<String, dynamic> json) => OwnerFFZ(
json['_id'] as int,
json['name'] as String,
json['display_name'] as String,
);
Role7TV _$Role7TVFromJson(Map<String, dynamic> json) => Role7TV(

View file

@ -182,34 +182,46 @@ void main() {
test("emote should parse correctly", () {
const sampleJson = """
{
"id": 317897,
"user": {
"id": 84534,
"name": "vulpeshd",
"displayName": "VulpesHD"
"id": 27081,
"name": "ZreknarF",
"height": 30,
"width": 40,
"public": false,
"hidden": false,
"modifier": false,
"offset": null,
"margins": null,
"css": null,
"owner": {
"_id": 1,
"name": "sirstendec",
"display_name": "SirStendec"
},
"code": "peepoPog",
"images": {
"1x": "https://cdn.betterttv.net/frankerfacez_emote/317897/1",
"2x": "https://cdn.betterttv.net/frankerfacez_emote/317897/2",
"4x": null
"urls": {
"1": "//cdn.frankerfacez.com/emote/27081/1",
"2": "//cdn.frankerfacez.com/emote/27081/2",
"4": "//cdn.frankerfacez.com/emote/27081/4"
},
"imageType": "png"
"status": 1,
"usage_count": 1,
"created_at": "2015-06-01T05:52:57.770Z",
"last_updated": "2015-06-01T05:53:03.744Z"
}
""";
final json = jsonDecode(sampleJson);
final emote = EmoteFFZ.fromJson(json);
expect(emote.id, 317897);
expect(emote.user.id, 84534);
expect(emote.user.name, "vulpeshd");
expect(emote.user.displayName, "VulpesHD");
expect(emote.code, "peepoPog");
expect(emote.images.url1x, "https://cdn.betterttv.net/frankerfacez_emote/317897/1");
expect(emote.images.url2x, "https://cdn.betterttv.net/frankerfacez_emote/317897/2");
expect(emote.images.url4x, null);
expect(emote.imageType, "png");
expect(emote.id, 27081);
expect(emote.height, 30);
expect(emote.width, 40);
expect(emote.name, 'ZreknarF');
expect(emote.owner.id, 1);
expect(emote.owner.name, 'sirstendec');
expect(emote.owner.displayName, 'SirStendec');
expect(emote.urls.url1x, '//cdn.frankerfacez.com/emote/27081/1');
expect(emote.urls.url2x, '//cdn.frankerfacez.com/emote/27081/2');
expect(emote.urls.url4x, '//cdn.frankerfacez.com/emote/27081/4');
});
});