Migrate 7TV emotes to V3 API (#287)

* Migrate 7TV global emotes to V3 API

* Migrate 7TV channel emotes to V3 API
This commit is contained in:
Tommy Chow 2023-11-01 13:34:21 -04:00 • committed by GitHub
parent 26c5bb2437
commit a981cec250
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 77 additions and 178 deletions

View file

@ -12,12 +12,12 @@ class SevenTVApi {
/// Returns a map of global 7TV emotes to their URL. /// Returns a map of global 7TV emotes to their URL.
Future<List<Emote>> getEmotesGlobal() async { Future<List<Emote>> getEmotesGlobal() async {
final url = Uri.parse('https://api.7tv.app/v2/emotes/global'); final url = Uri.parse('https://7tv.io/v3/emote-sets/global');
final response = await _client.get(url); final response = await _client.get(url);
if (response.statusCode == 200) { if (response.statusCode == 200) {
final decoded = jsonDecode(response.body) as List; final decoded = jsonDecode(response.body)['emotes'] as List;
final emotes = decoded.map((emote) => Emote7TV.fromJson(emote)); final emotes = decoded.map((emote) => Emote7TV.fromJson(emote['data']));
return emotes return emotes
.map((emote) => Emote.from7TV(emote, EmoteType.sevenTVGlobal)) .map((emote) => Emote.from7TV(emote, EmoteType.sevenTVGlobal))
@ -29,12 +29,12 @@ class SevenTVApi {
/// Returns a map of a channel's 7TV emotes to their URL. /// Returns a map of a channel's 7TV emotes to their URL.
Future<List<Emote>> getEmotesChannel({required String id}) async { Future<List<Emote>> getEmotesChannel({required String id}) async {
final url = Uri.parse('https://api.7tv.app/v2/users/$id/emotes'); final url = Uri.parse('https://7tv.io/v3/users/twitch/$id');
final response = await _client.get(url); final response = await _client.get(url);
if (response.statusCode == 200) { if (response.statusCode == 200) {
final decoded = jsonDecode(response.body) as List; final decoded = jsonDecode(response.body)['emote_set']['emotes'] as List;
final emotes = decoded.map((emote) => Emote7TV.fromJson(emote)); final emotes = decoded.map((emote) => Emote7TV.fromJson(emote['data']));
return emotes return emotes
.map((emote) => Emote.from7TV(emote, EmoteType.sevenTVChannel)) .map((emote) => Emote.from7TV(emote, EmoteType.sevenTVChannel))

View file

@ -124,6 +124,7 @@ class _MyAppState extends State<MyApp> {
final themes = FrostyThemes(); final themes = FrostyThemes();
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Frosty', title: 'Frosty',
theme: themes.light, theme: themes.light,
darkTheme: settingsStore.themeType == ThemeType.black darkTheme: settingsStore.themeType == ThemeType.black

View file

@ -107,24 +107,54 @@ class EmoteFFZ {
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake) @JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
class Emote7TV { class Emote7TV {
final String id;
final String name; final String name;
final List<String> visibilitySimple; final List<String>? tags;
final List<int> width; final Emote7TVHost host;
final List<int> height;
final List<List<String>> urls;
const Emote7TV( const Emote7TV(
this.id,
this.name, this.name,
this.visibilitySimple, this.tags,
this.width, this.host,
this.height,
this.urls,
); );
factory Emote7TV.fromJson(Map<String, dynamic> json) => factory Emote7TV.fromJson(Map<String, dynamic> json) =>
_$Emote7TVFromJson(json); _$Emote7TVFromJson(json);
} }
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
class Emote7TVHost {
final String url;
final List<Emote7TVFile> files;
Emote7TVHost(
this.url,
this.files,
);
factory Emote7TVHost.fromJson(Map<String, dynamic> json) =>
_$Emote7TVHostFromJson(json);
}
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
class Emote7TVFile {
final String name;
final int width;
final int height;
final String format;
Emote7TVFile(
this.name,
this.width,
this.height,
this.format,
);
factory Emote7TVFile.fromJson(Map<String, dynamic> json) =>
_$Emote7TVFileFromJson(json);
}
/// The common emote class. /// The common emote class.
@JsonSerializable() @JsonSerializable()
class Emote { class Emote {
@ -171,14 +201,22 @@ class Emote {
type: type, type: type,
); );
factory Emote.from7TV(Emote7TV emote, EmoteType type) => Emote( factory Emote.from7TV(Emote7TV emote, EmoteType type) {
name: emote.name, final url = emote.host.url;
width: emote.width.first, // Flutter doesn't support AVIF yet.
height: emote.height.first, final file = emote.host.files.reversed.firstWhere(
zeroWidth: emote.visibilitySimple.contains('ZERO_WIDTH'), (file) => file.format != 'AVIF' && file.name.contains('4x'),
url: emote.urls[3][1], );
type: type,
); return Emote(
name: emote.name,
width: emote.host.files.first.width,
height: emote.host.files.first.height,
zeroWidth: emote.tags?.contains('zerowidth') ?? false,
url: 'https:$url/${file.name}',
type: type,
);
}
factory Emote.fromJson(Map<String, dynamic> json) => _$EmoteFromJson(json); factory Emote.fromJson(Map<String, dynamic> json) => _$EmoteFromJson(json);
Map<String, dynamic> toJson() => _$EmoteToJson(this); Map<String, dynamic> toJson() => _$EmoteToJson(this);

View file

@ -52,17 +52,26 @@ EmoteFFZ _$EmoteFFZFromJson(Map<String, dynamic> json) => EmoteFFZ(
); );
Emote7TV _$Emote7TVFromJson(Map<String, dynamic> json) => Emote7TV( Emote7TV _$Emote7TVFromJson(Map<String, dynamic> json) => Emote7TV(
json['id'] as String,
json['name'] as String, json['name'] as String,
(json['visibility_simple'] as List<dynamic>) (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),
.map((e) => e as String) Emote7TVHost.fromJson(json['host'] as Map<String, dynamic>),
.toList(), );
(json['width'] as List<dynamic>).map((e) => e as int).toList(),
(json['height'] as List<dynamic>).map((e) => e as int).toList(), Emote7TVHost _$Emote7TVHostFromJson(Map<String, dynamic> json) => Emote7TVHost(
(json['urls'] as List<dynamic>) json['url'] as String,
.map((e) => (e as List<dynamic>).map((e) => e as String).toList()) (json['files'] as List<dynamic>)
.map((e) => Emote7TVFile.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
); );
Emote7TVFile _$Emote7TVFileFromJson(Map<String, dynamic> json) => Emote7TVFile(
json['name'] as String,
json['width'] as int,
json['height'] as int,
json['format'] as String,
);
Emote _$EmoteFromJson(Map<String, dynamic> json) => Emote( Emote _$EmoteFromJson(Map<String, dynamic> json) => Emote(
name: json['name'] as String, name: json['name'] as String,
width: json['width'] as int?, width: json['width'] as int?,

View file

@ -198,153 +198,4 @@ void main() {
expect(emote.urls.url4x, '//cdn.frankerfacez.com/emote/27081/4'); expect(emote.urls.url4x, '//cdn.frankerfacez.com/emote/27081/4');
}); });
}); });
group('7TV', () {
test('global emote should parse correctly', () {
const sampleGlobal7TVEmote = '''
{
"id": "603ca884faf3a00014dff0ab",
"name": "gachiBASS",
"owner": null,
"visibility": 2,
"visibility_simple": [
"GLOBAL"
],
"mime": "image/gif",
"status": 3,
"tags": [
],
"width": [
32,
48,
76,
128
],
"height": [
32,
48,
76,
128
],
"urls": [
[
"1",
"https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/1x"
],
[
"2",
"https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/2x"
],
[
"3",
"https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/3x"
],
[
"4",
"https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/4x"
]
]
}
''';
final json = jsonDecode(sampleGlobal7TVEmote);
final emote = Emote7TV.fromJson(json);
expect(emote.name, 'gachiBASS');
expect(emote.visibilitySimple, ['GLOBAL']);
expect(emote.width, [32, 48, 76, 128]);
expect(emote.height, [32, 48, 76, 128]);
final urls = [
['1', 'https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/1x'],
['2', 'https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/2x'],
['3', 'https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/3x'],
['4', 'https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/4x'],
];
expect(emote.urls, urls);
});
test('channel emote should parse correctly', () {
const sampleEmote = '''
{
"id": "603caea243b9e100141caf4f",
"name": "TrollDespair",
"owner": {
"id": "603cae2496832ffa78c758cd",
"twitch_id": "",
"login": "swyfty_",
"display_name": "swyfty_",
"role": {
"id": "000000000000000000000000",
"name": "",
"position": 0,
"color": 0,
"allowed": 523,
"denied": 0,
"default": true
}
},
"visibility": 0,
"visibility_simple": [
],
"mime": "image/png",
"status": 3,
"tags": [
],
"width": [
32,
48,
76,
128
],
"height": [
32,
48,
76,
128
],
"urls": [
[
"1",
"https://cdn.7tv.app/emote/603caea243b9e100141caf4f/1x"
],
[
"2",
"https://cdn.7tv.app/emote/603caea243b9e100141caf4f/2x"
],
[
"3",
"https://cdn.7tv.app/emote/603caea243b9e100141caf4f/3x"
],
[
"4",
"https://cdn.7tv.app/emote/603caea243b9e100141caf4f/4x"
]
]
}
''';
final json = jsonDecode(sampleEmote);
final emote = Emote7TV.fromJson(json);
expect(emote.name, 'TrollDespair');
expect(emote.visibilitySimple, []);
expect(emote.width, [32, 48, 76, 128]);
expect(emote.height, [32, 48, 76, 128]);
final urls = [
['1', 'https://cdn.7tv.app/emote/603caea243b9e100141caf4f/1x'],
['2', 'https://cdn.7tv.app/emote/603caea243b9e100141caf4f/2x'],
['3', 'https://cdn.7tv.app/emote/603caea243b9e100141caf4f/3x'],
['4', 'https://cdn.7tv.app/emote/603caea243b9e100141caf4f/4x'],
];
expect(emote.urls, urls);
});
});
} }