From a981cec2504e560e394103b64d05fb1bf51892e2 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Wed, 1 Nov 2023 13:34:21 -0400 Subject: [PATCH] Migrate 7TV emotes to V3 API (#287) * Migrate 7TV global emotes to V3 API * Migrate 7TV channel emotes to V3 API --- lib/apis/seventv_api.dart | 12 +-- lib/main.dart | 1 + lib/models/emotes.dart | 70 ++++++++++++++---- lib/models/emotes.g.dart | 23 ++++-- test/emote_test.dart | 149 -------------------------------------- 5 files changed, 77 insertions(+), 178 deletions(-) diff --git a/lib/apis/seventv_api.dart b/lib/apis/seventv_api.dart index e772cee5..b890445e 100644 --- a/lib/apis/seventv_api.dart +++ b/lib/apis/seventv_api.dart @@ -12,12 +12,12 @@ class SevenTVApi { /// Returns a map of global 7TV emotes to their URL. Future> 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); if (response.statusCode == 200) { - final decoded = jsonDecode(response.body) as List; - final emotes = decoded.map((emote) => Emote7TV.fromJson(emote)); + final decoded = jsonDecode(response.body)['emotes'] as List; + final emotes = decoded.map((emote) => Emote7TV.fromJson(emote['data'])); return emotes .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. Future> 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); if (response.statusCode == 200) { - final decoded = jsonDecode(response.body) as List; - final emotes = decoded.map((emote) => Emote7TV.fromJson(emote)); + final decoded = jsonDecode(response.body)['emote_set']['emotes'] as List; + final emotes = decoded.map((emote) => Emote7TV.fromJson(emote['data'])); return emotes .map((emote) => Emote.from7TV(emote, EmoteType.sevenTVChannel)) diff --git a/lib/main.dart b/lib/main.dart index 06c2a135..940ff996 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -124,6 +124,7 @@ class _MyAppState extends State { final themes = FrostyThemes(); return MaterialApp( + debugShowCheckedModeBanner: false, title: 'Frosty', theme: themes.light, darkTheme: settingsStore.themeType == ThemeType.black diff --git a/lib/models/emotes.dart b/lib/models/emotes.dart index 4a93ed8f..15d11a0d 100644 --- a/lib/models/emotes.dart +++ b/lib/models/emotes.dart @@ -107,24 +107,54 @@ class EmoteFFZ { @JsonSerializable(createToJson: false, fieldRename: FieldRename.snake) class Emote7TV { + final String id; final String name; - final List visibilitySimple; - final List width; - final List height; - final List> urls; + final List? tags; + final Emote7TVHost host; const Emote7TV( + this.id, this.name, - this.visibilitySimple, - this.width, - this.height, - this.urls, + this.tags, + this.host, ); factory Emote7TV.fromJson(Map json) => _$Emote7TVFromJson(json); } +@JsonSerializable(createToJson: false, fieldRename: FieldRename.snake) +class Emote7TVHost { + final String url; + final List files; + + Emote7TVHost( + this.url, + this.files, + ); + + factory Emote7TVHost.fromJson(Map 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 json) => + _$Emote7TVFileFromJson(json); +} + /// The common emote class. @JsonSerializable() class Emote { @@ -171,14 +201,22 @@ class Emote { type: type, ); - factory Emote.from7TV(Emote7TV emote, EmoteType type) => Emote( - name: emote.name, - width: emote.width.first, - height: emote.height.first, - zeroWidth: emote.visibilitySimple.contains('ZERO_WIDTH'), - url: emote.urls[3][1], - type: type, - ); + factory Emote.from7TV(Emote7TV emote, EmoteType type) { + final url = emote.host.url; + // Flutter doesn't support AVIF yet. + final file = emote.host.files.reversed.firstWhere( + (file) => file.format != 'AVIF' && file.name.contains('4x'), + ); + + 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 json) => _$EmoteFromJson(json); Map toJson() => _$EmoteToJson(this); diff --git a/lib/models/emotes.g.dart b/lib/models/emotes.g.dart index 33f7349b..1d8fd99d 100644 --- a/lib/models/emotes.g.dart +++ b/lib/models/emotes.g.dart @@ -52,17 +52,26 @@ EmoteFFZ _$EmoteFFZFromJson(Map json) => EmoteFFZ( ); Emote7TV _$Emote7TVFromJson(Map json) => Emote7TV( + json['id'] as String, json['name'] as String, - (json['visibility_simple'] as List) - .map((e) => e as String) - .toList(), - (json['width'] as List).map((e) => e as int).toList(), - (json['height'] as List).map((e) => e as int).toList(), - (json['urls'] as List) - .map((e) => (e as List).map((e) => e as String).toList()) + (json['tags'] as List?)?.map((e) => e as String).toList(), + Emote7TVHost.fromJson(json['host'] as Map), + ); + +Emote7TVHost _$Emote7TVHostFromJson(Map json) => Emote7TVHost( + json['url'] as String, + (json['files'] as List) + .map((e) => Emote7TVFile.fromJson(e as Map)) .toList(), ); +Emote7TVFile _$Emote7TVFileFromJson(Map json) => Emote7TVFile( + json['name'] as String, + json['width'] as int, + json['height'] as int, + json['format'] as String, + ); + Emote _$EmoteFromJson(Map json) => Emote( name: json['name'] as String, width: json['width'] as int?, diff --git a/test/emote_test.dart b/test/emote_test.dart index 4686e162..1d49dc54 100644 --- a/test/emote_test.dart +++ b/test/emote_test.dart @@ -198,153 +198,4 @@ void main() { 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); - }); - }); }