Skip to content

Commit a981cec

Browse files
authored
Migrate 7TV emotes to V3 API (#287)
* Migrate 7TV global emotes to V3 API * Migrate 7TV channel emotes to V3 API
1 parent 26c5bb2 commit a981cec

File tree

5 files changed

+77
-178
lines changed

5 files changed

+77
-178
lines changed

lib/apis/seventv_api.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ class SevenTVApi {
1212

1313
/// Returns a map of global 7TV emotes to their URL.
1414
Future<List<Emote>> getEmotesGlobal() async {
15-
final url = Uri.parse('https://api.7tv.app/v2/emotes/global');
15+
final url = Uri.parse('https://7tv.io/v3/emote-sets/global');
1616

1717
final response = await _client.get(url);
1818
if (response.statusCode == 200) {
19-
final decoded = jsonDecode(response.body) as List;
20-
final emotes = decoded.map((emote) => Emote7TV.fromJson(emote));
19+
final decoded = jsonDecode(response.body)['emotes'] as List;
20+
final emotes = decoded.map((emote) => Emote7TV.fromJson(emote['data']));
2121

2222
return emotes
2323
.map((emote) => Emote.from7TV(emote, EmoteType.sevenTVGlobal))
@@ -29,12 +29,12 @@ class SevenTVApi {
2929

3030
/// Returns a map of a channel's 7TV emotes to their URL.
3131
Future<List<Emote>> getEmotesChannel({required String id}) async {
32-
final url = Uri.parse('https://api.7tv.app/v2/users/$id/emotes');
32+
final url = Uri.parse('https://7tv.io/v3/users/twitch/$id');
3333

3434
final response = await _client.get(url);
3535
if (response.statusCode == 200) {
36-
final decoded = jsonDecode(response.body) as List;
37-
final emotes = decoded.map((emote) => Emote7TV.fromJson(emote));
36+
final decoded = jsonDecode(response.body)['emote_set']['emotes'] as List;
37+
final emotes = decoded.map((emote) => Emote7TV.fromJson(emote['data']));
3838

3939
return emotes
4040
.map((emote) => Emote.from7TV(emote, EmoteType.sevenTVChannel))

lib/main.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ class _MyAppState extends State<MyApp> {
124124
final themes = FrostyThemes();
125125

126126
return MaterialApp(
127+
debugShowCheckedModeBanner: false,
127128
title: 'Frosty',
128129
theme: themes.light,
129130
darkTheme: settingsStore.themeType == ThemeType.black

lib/models/emotes.dart

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -107,24 +107,54 @@ class EmoteFFZ {
107107

108108
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
109109
class Emote7TV {
110+
final String id;
110111
final String name;
111-
final List<String> visibilitySimple;
112-
final List<int> width;
113-
final List<int> height;
114-
final List<List<String>> urls;
112+
final List<String>? tags;
113+
final Emote7TVHost host;
115114

116115
const Emote7TV(
116+
this.id,
117117
this.name,
118-
this.visibilitySimple,
119-
this.width,
120-
this.height,
121-
this.urls,
118+
this.tags,
119+
this.host,
122120
);
123121

124122
factory Emote7TV.fromJson(Map<String, dynamic> json) =>
125123
_$Emote7TVFromJson(json);
126124
}
127125

126+
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
127+
class Emote7TVHost {
128+
final String url;
129+
final List<Emote7TVFile> files;
130+
131+
Emote7TVHost(
132+
this.url,
133+
this.files,
134+
);
135+
136+
factory Emote7TVHost.fromJson(Map<String, dynamic> json) =>
137+
_$Emote7TVHostFromJson(json);
138+
}
139+
140+
@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake)
141+
class Emote7TVFile {
142+
final String name;
143+
final int width;
144+
final int height;
145+
final String format;
146+
147+
Emote7TVFile(
148+
this.name,
149+
this.width,
150+
this.height,
151+
this.format,
152+
);
153+
154+
factory Emote7TVFile.fromJson(Map<String, dynamic> json) =>
155+
_$Emote7TVFileFromJson(json);
156+
}
157+
128158
/// The common emote class.
129159
@JsonSerializable()
130160
class Emote {
@@ -171,14 +201,22 @@ class Emote {
171201
type: type,
172202
);
173203

174-
factory Emote.from7TV(Emote7TV emote, EmoteType type) => Emote(
175-
name: emote.name,
176-
width: emote.width.first,
177-
height: emote.height.first,
178-
zeroWidth: emote.visibilitySimple.contains('ZERO_WIDTH'),
179-
url: emote.urls[3][1],
180-
type: type,
181-
);
204+
factory Emote.from7TV(Emote7TV emote, EmoteType type) {
205+
final url = emote.host.url;
206+
// Flutter doesn't support AVIF yet.
207+
final file = emote.host.files.reversed.firstWhere(
208+
(file) => file.format != 'AVIF' && file.name.contains('4x'),
209+
);
210+
211+
return Emote(
212+
name: emote.name,
213+
width: emote.host.files.first.width,
214+
height: emote.host.files.first.height,
215+
zeroWidth: emote.tags?.contains('zerowidth') ?? false,
216+
url: 'https:$url/${file.name}',
217+
type: type,
218+
);
219+
}
182220

183221
factory Emote.fromJson(Map<String, dynamic> json) => _$EmoteFromJson(json);
184222
Map<String, dynamic> toJson() => _$EmoteToJson(this);

lib/models/emotes.g.dart

Lines changed: 16 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/emote_test.dart

Lines changed: 0 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -198,153 +198,4 @@ void main() {
198198
expect(emote.urls.url4x, '//cdn.frankerfacez.com/emote/27081/4');
199199
});
200200
});
201-
202-
group('7TV', () {
203-
test('global emote should parse correctly', () {
204-
const sampleGlobal7TVEmote = '''
205-
{
206-
"id": "603ca884faf3a00014dff0ab",
207-
"name": "gachiBASS",
208-
"owner": null,
209-
"visibility": 2,
210-
"visibility_simple": [
211-
"GLOBAL"
212-
],
213-
"mime": "image/gif",
214-
"status": 3,
215-
"tags": [
216-
217-
],
218-
"width": [
219-
32,
220-
48,
221-
76,
222-
128
223-
],
224-
"height": [
225-
32,
226-
48,
227-
76,
228-
128
229-
],
230-
"urls": [
231-
[
232-
"1",
233-
"https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/1x"
234-
],
235-
[
236-
"2",
237-
"https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/2x"
238-
],
239-
[
240-
"3",
241-
"https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/3x"
242-
],
243-
[
244-
"4",
245-
"https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/4x"
246-
]
247-
]
248-
}
249-
''';
250-
251-
final json = jsonDecode(sampleGlobal7TVEmote);
252-
final emote = Emote7TV.fromJson(json);
253-
254-
expect(emote.name, 'gachiBASS');
255-
expect(emote.visibilitySimple, ['GLOBAL']);
256-
expect(emote.width, [32, 48, 76, 128]);
257-
expect(emote.height, [32, 48, 76, 128]);
258-
259-
final urls = [
260-
['1', 'https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/1x'],
261-
['2', 'https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/2x'],
262-
['3', 'https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/3x'],
263-
['4', 'https://cdn.7tv.app/emote/603ca884faf3a00014dff0ab/4x'],
264-
];
265-
266-
expect(emote.urls, urls);
267-
});
268-
269-
test('channel emote should parse correctly', () {
270-
const sampleEmote = '''
271-
{
272-
"id": "603caea243b9e100141caf4f",
273-
"name": "TrollDespair",
274-
"owner": {
275-
"id": "603cae2496832ffa78c758cd",
276-
"twitch_id": "",
277-
"login": "swyfty_",
278-
"display_name": "swyfty_",
279-
"role": {
280-
"id": "000000000000000000000000",
281-
"name": "",
282-
"position": 0,
283-
"color": 0,
284-
"allowed": 523,
285-
"denied": 0,
286-
"default": true
287-
}
288-
},
289-
"visibility": 0,
290-
"visibility_simple": [
291-
292-
],
293-
"mime": "image/png",
294-
"status": 3,
295-
"tags": [
296-
297-
],
298-
"width": [
299-
32,
300-
48,
301-
76,
302-
128
303-
],
304-
"height": [
305-
32,
306-
48,
307-
76,
308-
128
309-
],
310-
"urls": [
311-
[
312-
"1",
313-
"https://cdn.7tv.app/emote/603caea243b9e100141caf4f/1x"
314-
],
315-
[
316-
"2",
317-
"https://cdn.7tv.app/emote/603caea243b9e100141caf4f/2x"
318-
],
319-
[
320-
"3",
321-
"https://cdn.7tv.app/emote/603caea243b9e100141caf4f/3x"
322-
],
323-
[
324-
"4",
325-
"https://cdn.7tv.app/emote/603caea243b9e100141caf4f/4x"
326-
]
327-
]
328-
}
329-
''';
330-
331-
final json = jsonDecode(sampleEmote);
332-
final emote = Emote7TV.fromJson(json);
333-
334-
expect(emote.name, 'TrollDespair');
335-
336-
expect(emote.visibilitySimple, []);
337-
expect(emote.width, [32, 48, 76, 128]);
338-
expect(emote.height, [32, 48, 76, 128]);
339-
340-
final urls = [
341-
['1', 'https://cdn.7tv.app/emote/603caea243b9e100141caf4f/1x'],
342-
['2', 'https://cdn.7tv.app/emote/603caea243b9e100141caf4f/2x'],
343-
['3', 'https://cdn.7tv.app/emote/603caea243b9e100141caf4f/3x'],
344-
['4', 'https://cdn.7tv.app/emote/603caea243b9e100141caf4f/4x'],
345-
];
346-
347-
expect(emote.urls, urls);
348-
});
349-
});
350201
}

0 commit comments

Comments
 (0)