From f3e4a61b51525d28cb262d5f0e6cd580f6e0cb36 Mon Sep 17 00:00:00 2001 From: LezdCS Date: Tue, 28 May 2024 17:37:56 +0900 Subject: [PATCH 01/11] read Youtube Chat messages --- lib/src/core/utils/youtube_chat.dart | 97 ++++++++++++++++++++++++++++ pubspec.lock | 2 +- pubspec.yaml | 1 + 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 lib/src/core/utils/youtube_chat.dart diff --git a/lib/src/core/utils/youtube_chat.dart b/lib/src/core/utils/youtube_chat.dart new file mode 100644 index 00000000..adb5b58f --- /dev/null +++ b/lib/src/core/utils/youtube_chat.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:html/parser.dart' as parser; + +// Function to fetch the initial continuation token +Future fetchInitialContinuationToken(String videoId) async { + var headers = { + 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'accept-language': 'en-US,en;q=0.9', + 'cache-control': 'no-cache', + 'pragma': 'no-cache', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' + }; + + final url = 'https://www.youtube.com/live_chat?is_popout=1&v=$videoId'; + try { + final response = await http.get(Uri.parse(url), headers: headers); + final document = parser.parse(response.body); + final scripts = document.getElementsByTagName('script'); + final ytInitialDataScript = scripts.firstWhere((script) => script.innerHtml.contains('ytInitialData')); + final scriptContent = ytInitialDataScript.innerHtml; + print(scriptContent); + // Extracting ytInitialData JSON from the script tag + final dataStart = scriptContent.indexOf('{'); + final dataEnd = scriptContent.lastIndexOf('}') + 1; + final jsonString = scriptContent.substring(dataStart, dataEnd); + final ytInitialData = json.decode(jsonString); + print(ytInitialData); + + // Extract continuation token from ytInitialData + final continuation = ytInitialData['contents']['liveChatRenderer']['continuations'][0]['invalidationContinuationData']['continuation']; + return continuation; + } catch (error) { + print('Error fetching initial continuation token: $error'); + return null; + } +} + +Future fetchChatMessages(String? continuationToken) async { + final body = json.encode({ + 'context': { + 'client': { + 'hl': 'en', + 'gl': 'JP', + 'remoteHost': '123.123.123.123', + 'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + 'clientName': 'WEB', + 'clientVersion': '2.20240411.09.00', + }, + 'user': { + 'lockedSafetyMode': false + } + }, + 'continuation': continuationToken + }); + + final options = { + 'headers': {'Content-Type': 'application/json'}, + }; + + const url = 'https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?prettyPrint=false'; + + try { + final response = await http.post(Uri.parse(url), headers: options['headers'] as Map?, body: body); + final data = json.decode(response.body); + print(data); + final messages = (data['continuationContents']['liveChatContinuation']['actions'] as List?)?.map((action) => (action['addChatItemAction']['item']['liveChatTextMessageRenderer']['message']['runs'] as List?)?.map((run) => run['text']).join('')).where((message) => message != null).toList(); + print("Fetched messages: $messages"); + + final newContinuationToken = data['continuationContents']['liveChatContinuation']['continuations'][0]['invalidationContinuationData']['continuation']; + if (newContinuationToken == null) { + print("No continuation token found, terminating."); + return null; + } + return newContinuationToken; + } catch (error) { + print('Error fetching chat messages: $error'); + return continuationToken; // Retry with the same token + } +} + +Future startFetchingChat(String videoId) async { + var continuationToken = await fetchInitialContinuationToken(videoId); + if (continuationToken == null) { + print("Failed to fetch initial continuation token."); + return; + } + + while (continuationToken != null) { + continuationToken = await fetchChatMessages(continuationToken); + await Future.delayed(Duration(seconds: 5)); // 5-second pause + } +} + +void main() { + startFetchingChat("l8PMl7tUDIE"); // Replace with video ID +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index ad2bb5fd..d4cfd202 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -546,7 +546,7 @@ packages: source: hosted version: "5.3.4" html: - dependency: transitive + dependency: "direct main" description: name: html sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" diff --git a/pubspec.yaml b/pubspec.yaml index 4ca74868..439dda26 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,7 @@ dependencies: uuid: ^3.0.7 talker_flutter: ^4.2.2 talker_dio_logger: ^4.2.2 + html: ^0.15.4 dependency_overrides: wakelock_windows: From 377658a23a7d6c32152ba546c27c3e60e5555a30 Mon Sep 17 00:00:00 2001 From: LezdCS Date: Tue, 28 May 2024 18:50:15 +0900 Subject: [PATCH 02/11] create YoutubeChat class --- lib/src/core/utils/youtube_chat.dart | 174 ++++++++++-------- .../domain/entities/chat/chat_message.dart | 1 + 2 files changed, 97 insertions(+), 78 deletions(-) diff --git a/lib/src/core/utils/youtube_chat.dart b/lib/src/core/utils/youtube_chat.dart index adb5b58f..02a8dc7a 100644 --- a/lib/src/core/utils/youtube_chat.dart +++ b/lib/src/core/utils/youtube_chat.dart @@ -1,97 +1,115 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:html/parser.dart' as parser; +import 'package:irllink/src/core/utils/globals.dart' as globals; -// Function to fetch the initial continuation token -Future fetchInitialContinuationToken(String videoId) async { - var headers = { - 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'accept-language': 'en-US,en;q=0.9', - 'cache-control': 'no-cache', - 'pragma': 'no-cache', - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' - }; +class YoutubeChat { + String videoId; - final url = 'https://www.youtube.com/live_chat?is_popout=1&v=$videoId'; - try { - final response = await http.get(Uri.parse(url), headers: headers); - final document = parser.parse(response.body); - final scripts = document.getElementsByTagName('script'); - final ytInitialDataScript = scripts.firstWhere((script) => script.innerHtml.contains('ytInitialData')); - final scriptContent = ytInitialDataScript.innerHtml; - print(scriptContent); - // Extracting ytInitialData JSON from the script tag - final dataStart = scriptContent.indexOf('{'); - final dataEnd = scriptContent.lastIndexOf('}') + 1; - final jsonString = scriptContent.substring(dataStart, dataEnd); - final ytInitialData = json.decode(jsonString); - print(ytInitialData); + YoutubeChat( + this.videoId, + ); - // Extract continuation token from ytInitialData - final continuation = ytInitialData['contents']['liveChatRenderer']['continuations'][0]['invalidationContinuationData']['continuation']; - return continuation; - } catch (error) { - print('Error fetching initial continuation token: $error'); - return null; + // Function to fetch the initial continuation token + Future fetchInitialContinuationToken(String videoId) async { + var headers = { + 'accept': + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'accept-language': 'en-US,en;q=0.9', + 'cache-control': 'no-cache', + 'pragma': 'no-cache', + 'user-agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' + }; + + final url = 'https://www.youtube.com/live_chat?is_popout=1&v=$videoId'; + try { + final response = await http.get(Uri.parse(url), headers: headers); + final document = parser.parse(response.body); + final scripts = document.getElementsByTagName('script'); + final ytInitialDataScript = scripts + .firstWhere((script) => script.innerHtml.contains('ytInitialData')); + final scriptContent = ytInitialDataScript.innerHtml; + // Extracting ytInitialData JSON from the script tag + final dataStart = scriptContent.indexOf('{'); + final dataEnd = scriptContent.lastIndexOf('}') + 1; + final jsonString = scriptContent.substring(dataStart, dataEnd); + final ytInitialData = json.decode(jsonString); + globals.talker?.debug(ytInitialData); + + // Extract continuation token from ytInitialData + final continuation = ytInitialData['contents']['liveChatRenderer'] + ['continuations'][0]['invalidationContinuationData']['continuation']; + return continuation; + } catch (error) { + globals.talker + ?.error('Error fetching initial continuation token: $error'); + return null; + } } -} -Future fetchChatMessages(String? continuationToken) async { - final body = json.encode({ - 'context': { - 'client': { - 'hl': 'en', - 'gl': 'JP', - 'remoteHost': '123.123.123.123', - 'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', - 'clientName': 'WEB', - 'clientVersion': '2.20240411.09.00', + Future fetchChatMessages(String? continuationToken) async { + final body = json.encode({ + 'context': { + 'client': { + 'hl': 'en', + 'gl': 'JP', + 'remoteHost': '123.123.123.123', + 'userAgent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + 'clientName': 'WEB', + 'clientVersion': '2.20240411.09.00', + }, + 'user': {'lockedSafetyMode': false} }, - 'user': { - 'lockedSafetyMode': false - } - }, - 'continuation': continuationToken - }); + 'continuation': continuationToken + }); - final options = { - 'headers': {'Content-Type': 'application/json'}, - }; + final options = { + 'headers': {'Content-Type': 'application/json'}, + }; - const url = 'https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?prettyPrint=false'; + const url = + 'https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?prettyPrint=false'; - try { - final response = await http.post(Uri.parse(url), headers: options['headers'] as Map?, body: body); - final data = json.decode(response.body); - print(data); - final messages = (data['continuationContents']['liveChatContinuation']['actions'] as List?)?.map((action) => (action['addChatItemAction']['item']['liveChatTextMessageRenderer']['message']['runs'] as List?)?.map((run) => run['text']).join('')).where((message) => message != null).toList(); - print("Fetched messages: $messages"); + try { + final response = await http.post(Uri.parse(url), + headers: options['headers'], body: body); + final data = json.decode(response.body); + final messages = (data['continuationContents']['liveChatContinuation'] + ['actions'] as List?) + ?.map((action) => (action['addChatItemAction']['item'] + ['liveChatTextMessageRenderer']['message']['runs'] as List?) + ?.map((run) => run['text']) + .join('')) + .where((message) => message != null) + .toList(); + globals.talker?.info("Fetched messages: $messages"); - final newContinuationToken = data['continuationContents']['liveChatContinuation']['continuations'][0]['invalidationContinuationData']['continuation']; - if (newContinuationToken == null) { - print("No continuation token found, terminating."); - return null; + final newContinuationToken = data['continuationContents'] + ['liveChatContinuation']['continuations'][0] + ['invalidationContinuationData']['continuation']; + if (newContinuationToken == null) { + globals.talker?.info('No continuation token found, terminating.'); + return null; + } + return newContinuationToken; + } catch (error) { + globals.talker?.error('Error fetching chat messages: $error'); + return continuationToken; // Retry with the same token } - return newContinuationToken; - } catch (error) { - print('Error fetching chat messages: $error'); - return continuationToken; // Retry with the same token } -} -Future startFetchingChat(String videoId) async { - var continuationToken = await fetchInitialContinuationToken(videoId); - if (continuationToken == null) { - print("Failed to fetch initial continuation token."); - return; - } + Future startFetchingChat(String videoId) async { + var continuationToken = await fetchInitialContinuationToken(videoId); + if (continuationToken == null) { + globals.talker?.error('Failed to fetch initial continuation token.'); + return; + } - while (continuationToken != null) { - continuationToken = await fetchChatMessages(continuationToken); - await Future.delayed(Duration(seconds: 5)); // 5-second pause + while (continuationToken != null) { + continuationToken = await fetchChatMessages(continuationToken); + await Future.delayed(const Duration(seconds: 5)); // 5-second pause + } } } - -void main() { - startFetchingChat("l8PMl7tUDIE"); // Replace with video ID -} \ No newline at end of file diff --git a/lib/src/domain/entities/chat/chat_message.dart b/lib/src/domain/entities/chat/chat_message.dart index 1aa5cdc8..aee9e519 100644 --- a/lib/src/domain/entities/chat/chat_message.dart +++ b/lib/src/domain/entities/chat/chat_message.dart @@ -19,6 +19,7 @@ enum EventType { enum Platform { twitch, kick, + youtube, } // ignore: must_be_immutable From 9d871189da2146d67e634afbc31d17b4896a89ed Mon Sep 17 00:00:00 2001 From: LezdCS Date: Wed, 29 May 2024 02:42:53 +0900 Subject: [PATCH 03/11] youtube add messge to chat stream --- lib/src/core/utils/youtube_chat.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/core/utils/youtube_chat.dart b/lib/src/core/utils/youtube_chat.dart index 02a8dc7a..51846382 100644 --- a/lib/src/core/utils/youtube_chat.dart +++ b/lib/src/core/utils/youtube_chat.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:html/parser.dart' as parser; @@ -5,6 +6,9 @@ import 'package:irllink/src/core/utils/globals.dart' as globals; class YoutubeChat { String videoId; + + final StreamController _chatStreamController = StreamController.broadcast(); + Stream get chatStream => _chatStreamController.stream; YoutubeChat( this.videoId, @@ -85,6 +89,9 @@ class YoutubeChat { .where((message) => message != null) .toList(); globals.talker?.info("Fetched messages: $messages"); + messages?.forEach((message) { + _chatStreamController.add(message); + }); final newContinuationToken = data['continuationContents'] ['liveChatContinuation']['continuations'][0] From d3ba9b0fd8213c92d6ae814a7dad5d062e88b8a1 Mon Sep 17 00:00:00 2001 From: LezdCS Date: Wed, 29 May 2024 16:20:29 +0900 Subject: [PATCH 04/11] working youtube chat messages --- lib/assets/kick/kickLogo.png | Bin 0 -> 1538 bytes lib/assets/youtube/youtubeLogo.png | Bin 0 -> 9626 bytes lib/src/core/utils/youtube_chat.dart | 24 ++++---- .../domain/entities/chat/chat_message.dart | 46 +++++++++++++- .../controllers/chat_view_controller.dart | 36 +++++++++++ .../chat_message/shared/message_row.dart | 57 ++++++++++++------ .../presentation/widgets/chats/chat_view.dart | 19 +++++- .../widgets/settings/chats_joined.dart | 44 +++++++++----- .../widgets/settings/stream_elements.dart | 3 +- pubspec.yaml | 2 + 10 files changed, 180 insertions(+), 51 deletions(-) create mode 100644 lib/assets/kick/kickLogo.png create mode 100644 lib/assets/youtube/youtubeLogo.png diff --git a/lib/assets/kick/kickLogo.png b/lib/assets/kick/kickLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..f98ca4a1ee3fbedb6306911a72b99c51f5d8ac4d GIT binary patch literal 1538 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 fetchInitialContinuationToken(String videoId) async { + Future fetchInitialContinuationToken() async { var headers = { 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', @@ -80,17 +81,16 @@ class YoutubeChat { final response = await http.post(Uri.parse(url), headers: options['headers'], body: body); final data = json.decode(response.body); - final messages = (data['continuationContents']['liveChatContinuation'] + + + final messagesData = (data['continuationContents']['liveChatContinuation'] ['actions'] as List?) ?.map((action) => (action['addChatItemAction']['item'] - ['liveChatTextMessageRenderer']['message']['runs'] as List?) - ?.map((run) => run['text']) - .join('')) - .where((message) => message != null) - .toList(); - globals.talker?.info("Fetched messages: $messages"); - messages?.forEach((message) { - _chatStreamController.add(message); + ['liveChatTextMessageRenderer'])); + + messagesData?.forEach((message) { + ChatMessage msg = ChatMessage.fromYoutube(message, videoId); + _chatStreamController.add(msg); }); final newContinuationToken = data['continuationContents'] @@ -107,8 +107,8 @@ class YoutubeChat { } } - Future startFetchingChat(String videoId) async { - var continuationToken = await fetchInitialContinuationToken(videoId); + Future startFetchingChat() async { + var continuationToken = await fetchInitialContinuationToken(); if (continuationToken == null) { globals.talker?.error('Failed to fetch initial continuation token.'); return; diff --git a/lib/src/domain/entities/chat/chat_message.dart b/lib/src/domain/entities/chat/chat_message.dart index aee9e519..e53cf3c3 100644 --- a/lib/src/domain/entities/chat/chat_message.dart +++ b/lib/src/domain/entities/chat/chat_message.dart @@ -165,11 +165,11 @@ class ChatMessage extends Equatable isVip: false, isDeleted: false, rawData: '', - eventType: null, //TODO: Add event type for KickMessage + eventType: null, badgesList: message.data.sender.identity.badges .map((badge) => ChatBadge.fromKick(channelId, badge, subBadges)) .toList(), - emotes: const {}, //TODO: Add emotes for KickMessage + emotes: const {}, platform: Platform.kick, channelId: channelId, @@ -187,6 +187,48 @@ class ChatMessage extends Equatable ); } + factory ChatMessage.fromYoutube(dynamic message, String videoId) { + String authorName = message['authorName']['simpleText']; + String id = message['id']; + String timestamp = message['timestampUsec']; + List? messages = (message['message']['runs'] as List?) + ?.map((run) => run['text']) + .where((message) => message != null) + .toList(); + return ChatMessage( + id: id, + authorId: '', + displayName: authorName, + username: authorName, + color: '#FFFFFF', + message: messages?[0], + timestamp: int.parse(timestamp), + isAction: false, + isSubscriber: false, + isModerator: false, + isVip: false, + isDeleted: false, + rawData: '', + eventType: null, + badgesList: const [], + emotes: const {}, + platform: Platform.youtube, + channelId: videoId, + + //implements + raidingChannelName: '', + badges: const [], + giftedName: '', + highlightType: null, + isGift: false, + months: '', + systemMessage: '', + tier: '', + totalBits: 0, + viewerCount: 0, + ); + } + factory ChatMessage.kickSub( KickSubscription sub, String channelId, List subBadges) { return ChatMessage( diff --git a/lib/src/presentation/controllers/chat_view_controller.dart b/lib/src/presentation/controllers/chat_view_controller.dart index 419631ac..eec65011 100644 --- a/lib/src/presentation/controllers/chat_view_controller.dart +++ b/lib/src/presentation/controllers/chat_view_controller.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:irllink/src/core/utils/constants.dart'; +import 'package:irllink/src/core/utils/youtube_chat.dart'; import 'package:irllink/src/domain/entities/chat/chat_emote.dart'; import 'package:irllink/src/domain/entities/chat/chat_message.dart'; import 'package:irllink/src/domain/entities/settings/chat_settings.dart'; @@ -49,6 +50,7 @@ class ChatViewController extends GetxController List twitchChats = []; List kickChats = []; + List youtubeChats = []; @override void onInit() async { @@ -248,6 +250,9 @@ class ChatViewController extends GetxController chatGroup.channels.where((e) => e.platform == Platform.twitch).toList(); List kickChannels = chatGroup.channels.where((e) => e.platform == Platform.kick).toList(); + List youtubeChannels = chatGroup.channels + .where((e) => e.platform == Platform.youtube) + .toList(); for (Channel tc in twitchChannels) { bool alreadyCreated = @@ -266,6 +271,15 @@ class ChatViewController extends GetxController createKickChat(kc); } + for (Channel kc in youtubeChannels) { + bool alreadyCreated = + kickChats.firstWhereOrNull((k) => k.username == kc.channel) != null; + if (alreadyCreated) { + return; + } + createYoutubeChat(kc.channel); + } + // Remove List twitchChatToRemove = twitchChats .where((tc) => @@ -374,6 +388,28 @@ class ChatViewController extends GetxController }); } + Future createYoutubeChat(String videoId) async { + YoutubeChat youtubeChat = YoutubeChat( + videoId, + ); + youtubeChat.startFetchingChat(); + youtubeChat.chatStream.listen((message) { + chatMessages.add(message); + + if (scrollController.hasClients && isAutoScrolldown.value) { + Timer(const Duration(milliseconds: 100), () { + if (isAutoScrolldown.value) { + scrollController.jumpTo( + scrollController.position.maxScrollExtent, + ); + } + }); + } + }); + youtubeChats.add(youtubeChat); + isChatConnected.value = true; + } + Future createKickChat(Channel kc) async { KickChat kickChat = KickChat( kc.channel, diff --git a/lib/src/presentation/widgets/chats/chat_message/shared/message_row.dart b/lib/src/presentation/widgets/chats/chat_message/shared/message_row.dart index 44e98c1f..476ce76a 100644 --- a/lib/src/presentation/widgets/chats/chat_message/shared/message_row.dart +++ b/lib/src/presentation/widgets/chats/chat_message/shared/message_row.dart @@ -41,13 +41,22 @@ class MessageRow extends StatelessWidget { List badges = []; badges.addAll(message.badgesList); if (showPlatformBadge) { - String kickBadge = - 'https://static.wikia.nocookie.net/logopedia/images/1/11/Kick_%28Icon%29.svg/revision/latest/scale-to-width-down/250?cb=20230622003955'; - String twitchBadge = 'https://pngimg.com/d/twitch_PNG18.png'; + String badge = ''; + switch (message.platform) { + case Platform.twitch: + badge = "lib/assets/twitch/twitch_logo.png"; + break; + case Platform.kick: + badge = "lib/assets/kick/kickLogo.png"; + break; + case Platform.youtube: + badge = "lib/assets/youtube/youtubeLogo.png"; + break; + } ChatBadge platformBadge = ChatBadge( - imageUrl1x: message.platform == Platform.kick ? kickBadge : twitchBadge, - imageUrl2x: message.platform == Platform.kick ? kickBadge : twitchBadge, - imageUrl4x: message.platform == Platform.kick ? kickBadge : twitchBadge, + imageUrl1x: badge, + imageUrl2x: badge, + imageUrl4x: badge, id: '', ); badges.insert(0, platformBadge); @@ -69,20 +78,28 @@ class MessageRow extends StatelessWidget { ), for (ChatBadge badge in badges) Container( - padding: const EdgeInsets.only(right: 4, top: 3), - child: Uri.parse(badge.imageUrl1x).isAbsolute - ? Image( - width: 18, - height: 18, - image: NetworkImage(badge.imageUrl1x), - filterQuality: FilterQuality.high, - ) - : SvgPicture.asset( - badge.imageUrl1x, - width: 18, - height: 18, - ), - ), + padding: const EdgeInsets.only(right: 4, top: 3), + child: Uri.parse(badge.imageUrl1x).isAbsolute + ? Image( + width: 18, + height: 18, + image: NetworkImage(badge.imageUrl1x), + filterQuality: FilterQuality.high, + ) + : badge.imageUrl1x.endsWith('.svg') + ? SvgPicture.asset( + badge.imageUrl1x, + width: 18, + height: 18, + ) + : Image( + width: 18, + height: 18, + image: AssetImage( + badge.imageUrl1x, + ), + filterQuality: FilterQuality.high, + )), AuthorName( isAction: message.isAction, username: message.username, diff --git a/lib/src/presentation/widgets/chats/chat_view.dart b/lib/src/presentation/widgets/chats/chat_view.dart index 9d601407..1ee42940 100644 --- a/lib/src/presentation/widgets/chats/chat_view.dart +++ b/lib/src/presentation/widgets/chats/chat_view.dart @@ -29,8 +29,8 @@ class ChatView extends StatelessWidget { if (controller == null) { return Container(); } - bool multiplePlatform = - controller.kickChats.isNotEmpty && controller.twitchChats.isNotEmpty; + bool multiplePlatform = atLeastTwoNotEmpty([controller.kickChats, controller.twitchChats, controller.youtubeChats]); + debugPrint('LOLLLLLLL: , $multiplePlatform'); return Obx( () => Stack(children: [ GestureDetector( @@ -190,3 +190,18 @@ class ChatView extends StatelessWidget { ); } } + +bool atLeastTwoNotEmpty(List lists) { + int count = 0; + + for (final list in lists) { + if (list.isNotEmpty) { + count++; + if (count >= 2) { + return true; + } + } + } + + return false; +} \ No newline at end of file diff --git a/lib/src/presentation/widgets/settings/chats_joined.dart b/lib/src/presentation/widgets/settings/chats_joined.dart index 11a23af6..5458296f 100644 --- a/lib/src/presentation/widgets/settings/chats_joined.dart +++ b/lib/src/presentation/widgets/settings/chats_joined.dart @@ -143,11 +143,18 @@ class ChatsJoined extends GetView { itemCount: group.channels.length, itemBuilder: (BuildContext context, int index) { Channel channel = group.channels[index]; - String kickBadge = - 'https://static.wikia.nocookie.net/logopedia/images/1/11/Kick_%28Icon%29.svg/revision/latest/scale-to-width-down/250?cb=20230622003955'; - String twitchBadge = 'https://pngimg.com/d/twitch_PNG18.png'; - String badge = - channel.platform == Platform.kick ? kickBadge : twitchBadge; + String badge = ''; + switch (channel.platform) { + case Platform.twitch: + badge = "lib/assets/twitch/twitch_logo.png"; + break; + case Platform.kick: + badge = "lib/assets/kick/kickLogo.png"; + break; + case Platform.youtube: + badge = "lib/assets/youtube/youtubeLogo.png"; + break; + } return Dismissible( direction: DismissDirection.endToStart, background: Container( @@ -189,7 +196,9 @@ class ChatsJoined extends GetView { Image( width: 18, height: 18, - image: NetworkImage(badge), + image: AssetImage( + badge, + ), filterQuality: FilterQuality.high, ), const Padding(padding: EdgeInsets.only(right: 8)), @@ -331,13 +340,18 @@ class ChatsJoined extends GetView { () => DropdownButton( value: selectedPlatform.toString(), items: List.generate(Platform.values.length, (index) { - String kickBadge = - 'https://static.wikia.nocookie.net/logopedia/images/1/11/Kick_%28Icon%29.svg/revision/latest/scale-to-width-down/250?cb=20230622003955'; - String twitchBadge = - 'https://pngimg.com/d/twitch_PNG18.png'; - String badge = Platform.values[index] == Platform.kick - ? kickBadge - : twitchBadge; + String badge = ''; + switch (Platform.values[index]) { + case Platform.twitch: + badge = "lib/assets/twitch/twitch_logo.png"; + break; + case Platform.kick: + badge = "lib/assets/kick/kickLogo.png"; + break; + case Platform.youtube: + badge = "lib/assets/youtube/youtubeLogo.png"; + break; + } return DropdownMenuItem( value: Platform.values[index].toString(), child: Row( @@ -346,7 +360,9 @@ class ChatsJoined extends GetView { Image( width: 18, height: 18, - image: NetworkImage(badge), + image: AssetImage( + badge, + ), filterQuality: FilterQuality.high, ), const Padding(padding: EdgeInsets.only(right: 8)), diff --git a/lib/src/presentation/widgets/settings/stream_elements.dart b/lib/src/presentation/widgets/settings/stream_elements.dart index 8c19b730..8b2d2f4b 100644 --- a/lib/src/presentation/widgets/settings/stream_elements.dart +++ b/lib/src/presentation/widgets/settings/stream_elements.dart @@ -235,7 +235,8 @@ class StreamElements extends GetView { children: [ Image( image: AssetImage( - "lib/assets/streamelements/seLogo.png"), + "lib/assets/streamelements/seLogo.png", + ), width: 30, ), SizedBox( diff --git a/pubspec.yaml b/pubspec.yaml index 439dda26..1381c508 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -117,7 +117,9 @@ flutter: - lib/assets/i18n/ - lib/assets/twitch/ - lib/assets/kick/badges/ + - lib/assets/kick/ - lib/assets/streamelements/ + - lib/assets/youtube/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. From 7a6c824c8fc7e4497cce2c7a97ec7d3ca954ec57 Mon Sep 17 00:00:00 2001 From: LezdCS Date: Wed, 29 May 2024 16:21:01 +0900 Subject: [PATCH 05/11] remove debugprint --- lib/src/presentation/widgets/chats/chat_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/presentation/widgets/chats/chat_view.dart b/lib/src/presentation/widgets/chats/chat_view.dart index 1ee42940..c2ce10a3 100644 --- a/lib/src/presentation/widgets/chats/chat_view.dart +++ b/lib/src/presentation/widgets/chats/chat_view.dart @@ -30,7 +30,6 @@ class ChatView extends StatelessWidget { return Container(); } bool multiplePlatform = atLeastTwoNotEmpty([controller.kickChats, controller.twitchChats, controller.youtubeChats]); - debugPrint('LOLLLLLLL: , $multiplePlatform'); return Obx( () => Stack(children: [ GestureDetector( From 8b15795b586d47c1d10b80bef734e966dcc14308 Mon Sep 17 00:00:00 2001 From: LezdCS Date: Wed, 29 May 2024 16:25:10 +0900 Subject: [PATCH 06/11] Fix delete youtube chat --- .../presentation/controllers/chat_view_controller.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/src/presentation/controllers/chat_view_controller.dart b/lib/src/presentation/controllers/chat_view_controller.dart index eec65011..db2a48f5 100644 --- a/lib/src/presentation/controllers/chat_view_controller.dart +++ b/lib/src/presentation/controllers/chat_view_controller.dart @@ -293,6 +293,12 @@ class ChatViewController extends GetxController .firstWhereOrNull((kCa) => kCa.channel == kc.username) == null) .toList(); + List youtubeChatToRemove = youtubeChats + .where((yc) => + youtubeChannels + .firstWhereOrNull((yCa) => yCa.channel == yc.videoId) == + null) + .toList(); for (TwitchChat t in twitchChatToRemove) { t.close(); @@ -303,6 +309,10 @@ class ChatViewController extends GetxController k.close(); kickChats.removeWhere((kc) => kc.username == k.username); } + + for (YoutubeChat y in youtubeChatToRemove) { + youtubeChats.removeWhere((yc) => yc.videoId == y.videoId); + } } void createTwitchChat(Channel tc) { From 463dd065b27b3658836f5a7aafb875dbd56706f6 Mon Sep 17 00:00:00 2001 From: LezdCS Date: Thu, 30 May 2024 15:15:44 +0900 Subject: [PATCH 07/11] Get youtube live video ID from user channel URL --- lib/src/core/utils/youtube_chat.dart | 94 ++++++++++++++----- .../controllers/chat_view_controller.dart | 10 +- .../widgets/settings/chats_joined.dart | 9 +- 3 files changed, 87 insertions(+), 26 deletions(-) diff --git a/lib/src/core/utils/youtube_chat.dart b/lib/src/core/utils/youtube_chat.dart index ee354e18..2ee314f0 100644 --- a/lib/src/core/utils/youtube_chat.dart +++ b/lib/src/core/utils/youtube_chat.dart @@ -1,13 +1,15 @@ import 'dart:async'; import 'dart:convert'; +import 'package:html/dom.dart'; import 'package:http/http.dart' as http; import 'package:html/parser.dart' as parser; +import 'package:http/http.dart'; import 'package:irllink/src/core/utils/globals.dart' as globals; import 'package:irllink/src/domain/entities/chat/chat_message.dart'; class YoutubeChat { String videoId; - + final StreamController _chatStreamController = StreamController.broadcast(); Stream get chatStream => _chatStreamController.stream; @@ -27,23 +29,22 @@ class YoutubeChat { 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' }; - final url = 'https://www.youtube.com/live_chat?is_popout=1&v=$videoId'; + String url = 'https://www.youtube.com/live_chat?is_popout=1&v=$videoId'; try { - final response = await http.get(Uri.parse(url), headers: headers); - final document = parser.parse(response.body); - final scripts = document.getElementsByTagName('script'); - final ytInitialDataScript = scripts + Response response = await http.get(Uri.parse(url), headers: headers); + Document document = parser.parse(response.body); + List scripts = document.getElementsByTagName('script'); + Element ytInitialDataScript = scripts .firstWhere((script) => script.innerHtml.contains('ytInitialData')); - final scriptContent = ytInitialDataScript.innerHtml; + String scriptContent = ytInitialDataScript.innerHtml; // Extracting ytInitialData JSON from the script tag - final dataStart = scriptContent.indexOf('{'); - final dataEnd = scriptContent.lastIndexOf('}') + 1; - final jsonString = scriptContent.substring(dataStart, dataEnd); - final ytInitialData = json.decode(jsonString); - globals.talker?.debug(ytInitialData); + int dataStart = scriptContent.indexOf('{'); + int dataEnd = scriptContent.lastIndexOf('}') + 1; + String jsonString = scriptContent.substring(dataStart, dataEnd); + dynamic ytInitialData = json.decode(jsonString); // Extract continuation token from ytInitialData - final continuation = ytInitialData['contents']['liveChatRenderer'] + dynamic continuation = ytInitialData['contents']['liveChatRenderer'] ['continuations'][0]['invalidationContinuationData']['continuation']; return continuation; } catch (error) { @@ -54,7 +55,7 @@ class YoutubeChat { } Future fetchChatMessages(String? continuationToken) async { - final body = json.encode({ + String body = json.encode({ 'context': { 'client': { 'hl': 'en', @@ -70,7 +71,7 @@ class YoutubeChat { 'continuation': continuationToken }); - final options = { + Map> options = { 'headers': {'Content-Type': 'application/json'}, }; @@ -78,22 +79,20 @@ class YoutubeChat { 'https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?prettyPrint=false'; try { - final response = await http.post(Uri.parse(url), + Response response = await http.post(Uri.parse(url), headers: options['headers'], body: body); - final data = json.decode(response.body); - + dynamic data = json.decode(response.body); - final messagesData = (data['continuationContents']['liveChatContinuation'] + Iterable? messagesData = (data['continuationContents']['liveChatContinuation'] ['actions'] as List?) ?.map((action) => (action['addChatItemAction']['item'] - ['liveChatTextMessageRenderer'])); - + ['liveChatTextMessageRenderer'])); messagesData?.forEach((message) { ChatMessage msg = ChatMessage.fromYoutube(message, videoId); _chatStreamController.add(msg); }); - final newContinuationToken = data['continuationContents'] + dynamic newContinuationToken = data['continuationContents'] ['liveChatContinuation']['continuations'][0] ['invalidationContinuationData']['continuation']; if (newContinuationToken == null) { @@ -120,3 +119,54 @@ class YoutubeChat { } } } + +Future getLiveVideoId(String channelURL) async { + // Send GET request to the YouTube channel's live streams page + var response = await http.get(Uri.parse(channelURL)); + if (response.statusCode != 200) { + globals.talker?.error( + 'Failed to retrieve the page. Status code: ${response.statusCode}'); + return null; + } + + // Extract the ytInitialData JSON data from the page content + var match = RegExp(r'ytInitialData\s*=\s*({.*?});') + .firstMatch(response.body); + if (match == null) { + globals.talker?.error('Failed to find ytInitialData in the page content.'); + return null; + } + + var ytInitialData = json.decode(match.group(1)!); + + // Navigate through the JSON data to find the live video ID + try { + var tabs = + ytInitialData['contents']['twoColumnBrowseResultsRenderer']['tabs']; + for (var tab in tabs) { + if (tab is Map && + tab['tabRenderer'] != null && + tab['tabRenderer']['selected'] != null && + tab['tabRenderer']['content'] != null) { + var contents = + tab['tabRenderer']['content']['richGridRenderer']['contents']; + for (var content in contents) { + if (content is Map && + content['richItemRenderer'] != null && + content['richItemRenderer']['content'] != null && + content['richItemRenderer']['content']['videoRenderer'] != null) { + var videoId = content['richItemRenderer']['content'] + ['videoRenderer']['videoId']; + return videoId; + } + } + } + } + } catch (e) { + globals.talker?.error('Error parsing ytInitialData: $e'); + return null; + } + + globals.talker?.info('No live video found.'); + return null; +} diff --git a/lib/src/presentation/controllers/chat_view_controller.dart b/lib/src/presentation/controllers/chat_view_controller.dart index db2a48f5..2849645a 100644 --- a/lib/src/presentation/controllers/chat_view_controller.dart +++ b/lib/src/presentation/controllers/chat_view_controller.dart @@ -398,14 +398,20 @@ class ChatViewController extends GetxController }); } - Future createYoutubeChat(String videoId) async { + Future createYoutubeChat(String channelId) async { + String? videoId = await getLiveVideoId(channelId); + if (videoId == null) { + globals.talker?.error('VideoID not found for the channel: $channelId'); + return; + } + globals.talker?.error('videoId: $videoId'); + YoutubeChat youtubeChat = YoutubeChat( videoId, ); youtubeChat.startFetchingChat(); youtubeChat.chatStream.listen((message) { chatMessages.add(message); - if (scrollController.hasClients && isAutoScrolldown.value) { Timer(const Duration(milliseconds: 100), () { if (isAutoScrolldown.value) { diff --git a/lib/src/presentation/widgets/settings/chats_joined.dart b/lib/src/presentation/widgets/settings/chats_joined.dart index 5458296f..ae3366aa 100644 --- a/lib/src/presentation/widgets/settings/chats_joined.dart +++ b/lib/src/presentation/widgets/settings/chats_joined.dart @@ -202,8 +202,13 @@ class ChatsJoined extends GetView { filterQuality: FilterQuality.high, ), const Padding(padding: EdgeInsets.only(right: 8)), - Text(channel.channel, - style: const TextStyle(fontSize: 18)), + Flexible( + child: Text( + channel.channel, + style: const TextStyle(fontSize: 18), + overflow: TextOverflow.ellipsis, + ), + ), ], ), ), From b6810301c8352a9d9aea6c9ba2868a4b837ec7e8 Mon Sep 17 00:00:00 2001 From: LezdCS Date: Thu, 30 May 2024 15:26:04 +0900 Subject: [PATCH 08/11] fix getLiveVideoId --- lib/src/core/utils/youtube_chat.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/src/core/utils/youtube_chat.dart b/lib/src/core/utils/youtube_chat.dart index 2ee314f0..6b447a37 100644 --- a/lib/src/core/utils/youtube_chat.dart +++ b/lib/src/core/utils/youtube_chat.dart @@ -83,8 +83,8 @@ class YoutubeChat { headers: options['headers'], body: body); dynamic data = json.decode(response.body); - Iterable? messagesData = (data['continuationContents']['liveChatContinuation'] - ['actions'] as List?) + Iterable? messagesData = (data['continuationContents'] + ['liveChatContinuation']['actions'] as List?) ?.map((action) => (action['addChatItemAction']['item'] ['liveChatTextMessageRenderer'])); messagesData?.forEach((message) { @@ -154,7 +154,10 @@ Future getLiveVideoId(String channelURL) async { if (content is Map && content['richItemRenderer'] != null && content['richItemRenderer']['content'] != null && - content['richItemRenderer']['content']['videoRenderer'] != null) { + content['richItemRenderer']['content']['videoRenderer'] != null && + content['richItemRenderer']['content']['videoRenderer'] + ['upcomingEventData'] == + null) { var videoId = content['richItemRenderer']['content'] ['videoRenderer']['videoId']; return videoId; From eab649a4f49dd4ee66c10a244c4eb1fb888b5304 Mon Sep 17 00:00:00 2001 From: LezdCS Date: Thu, 30 May 2024 15:42:03 +0900 Subject: [PATCH 09/11] fix ytb chat --- lib/src/core/utils/youtube_chat.dart | 11 ++++++++++- lib/src/domain/entities/chat/chat_message.dart | 12 ++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/src/core/utils/youtube_chat.dart b/lib/src/core/utils/youtube_chat.dart index 6b447a37..3045647a 100644 --- a/lib/src/core/utils/youtube_chat.dart +++ b/lib/src/core/utils/youtube_chat.dart @@ -83,12 +83,21 @@ class YoutubeChat { headers: options['headers'], body: body); dynamic data = json.decode(response.body); + Iterable? messagesData = (data['continuationContents'] ['liveChatContinuation']['actions'] as List?) ?.map((action) => (action['addChatItemAction']['item'] ['liveChatTextMessageRenderer'])); + messagesData?.forEach((message) { - ChatMessage msg = ChatMessage.fromYoutube(message, videoId); + if(message['message'] == null) return; + List? messages = (message['message']['runs'] as List?) + ?.map((run) => run['text']) + .where((message) => message != null) + .toList(); + if(messages == null) return; + if(messages.isEmpty) return; + ChatMessage msg = ChatMessage.fromYoutube(message, messages, videoId); _chatStreamController.add(msg); }); diff --git a/lib/src/domain/entities/chat/chat_message.dart b/lib/src/domain/entities/chat/chat_message.dart index e53cf3c3..43b50a1a 100644 --- a/lib/src/domain/entities/chat/chat_message.dart +++ b/lib/src/domain/entities/chat/chat_message.dart @@ -187,14 +187,10 @@ class ChatMessage extends Equatable ); } - factory ChatMessage.fromYoutube(dynamic message, String videoId) { - String authorName = message['authorName']['simpleText']; - String id = message['id']; - String timestamp = message['timestampUsec']; - List? messages = (message['message']['runs'] as List?) - ?.map((run) => run['text']) - .where((message) => message != null) - .toList(); + factory ChatMessage.fromYoutube(dynamic messageRaw, List? messages, String videoId) { + String authorName = messageRaw['authorName']['simpleText']; + String id = messageRaw['id']; + String timestamp = messageRaw['timestampUsec']; return ChatMessage( id: id, authorId: '', From 1210178f4d340d3a6bdc1e4503c1bfbe1f7ecbcf Mon Sep 17 00:00:00 2001 From: LezdCS Date: Thu, 30 May 2024 15:42:50 +0900 Subject: [PATCH 10/11] remove useless log --- lib/src/presentation/controllers/chat_view_controller.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/presentation/controllers/chat_view_controller.dart b/lib/src/presentation/controllers/chat_view_controller.dart index 2849645a..6036335a 100644 --- a/lib/src/presentation/controllers/chat_view_controller.dart +++ b/lib/src/presentation/controllers/chat_view_controller.dart @@ -404,7 +404,6 @@ class ChatViewController extends GetxController globals.talker?.error('VideoID not found for the channel: $channelId'); return; } - globals.talker?.error('videoId: $videoId'); YoutubeChat youtubeChat = YoutubeChat( videoId, From 296e0b69bf3dbb457235a71b1b98af351cb171c5 Mon Sep 17 00:00:00 2001 From: LezdCS Date: Thu, 30 May 2024 15:49:04 +0900 Subject: [PATCH 11/11] fix close youtube chat --- lib/src/core/utils/youtube_chat.dart | 5 +++++ lib/src/presentation/controllers/chat_view_controller.dart | 1 + 2 files changed, 6 insertions(+) diff --git a/lib/src/core/utils/youtube_chat.dart b/lib/src/core/utils/youtube_chat.dart index 3045647a..ca889357 100644 --- a/lib/src/core/utils/youtube_chat.dart +++ b/lib/src/core/utils/youtube_chat.dart @@ -17,6 +17,10 @@ class YoutubeChat { this.videoId, ); + void closeStream() { + _chatStreamController.close(); + } + // Function to fetch the initial continuation token Future fetchInitialContinuationToken() async { var headers = { @@ -123,6 +127,7 @@ class YoutubeChat { } while (continuationToken != null) { + if(_chatStreamController.isClosed) return; continuationToken = await fetchChatMessages(continuationToken); await Future.delayed(const Duration(seconds: 5)); // 5-second pause } diff --git a/lib/src/presentation/controllers/chat_view_controller.dart b/lib/src/presentation/controllers/chat_view_controller.dart index 6036335a..a67799e5 100644 --- a/lib/src/presentation/controllers/chat_view_controller.dart +++ b/lib/src/presentation/controllers/chat_view_controller.dart @@ -311,6 +311,7 @@ class ChatViewController extends GetxController } for (YoutubeChat y in youtubeChatToRemove) { + y.closeStream(); youtubeChats.removeWhere((yc) => yc.videoId == y.videoId); } }