From 7beb52d62d7014ce6852f87a9be1dd339bec8d9c Mon Sep 17 00:00:00 2001 From: Hemant KArya <65885023+HemantKArya@users.noreply.github.com> Date: Thu, 21 Mar 2024 02:10:13 +0530 Subject: [PATCH] youtube charts on home page --- lib/blocs/explore/cubit/explore_cubit.dart | 16 +++++ lib/blocs/explore/cubit/explore_state.dart | 21 ++++++ lib/model/yt_music_model.dart | 1 - lib/repository/Youtube/yt_charts_home.dart | 80 ++++++++++++++++++++++ lib/repository/Youtube/yt_music_api.dart | 7 +- lib/repository/Youtube/yt_music_home.dart | 74 ++++++++++++++++++++ lib/repository/Youtube/ytmusic_format.dart | 4 +- lib/screens/screen/chart/chart_view.dart | 1 + lib/screens/screen/explore_screen.dart | 41 ++++++----- lib/screens/widgets/chart_list_tile.dart | 37 +++++----- lib/screens/widgets/tabList_widget.dart | 73 +++++++++++++------- pubspec.yaml | 1 + 12 files changed, 291 insertions(+), 65 deletions(-) create mode 100644 lib/blocs/explore/cubit/explore_cubit.dart create mode 100644 lib/blocs/explore/cubit/explore_state.dart create mode 100644 lib/repository/Youtube/yt_charts_home.dart create mode 100644 lib/repository/Youtube/yt_music_home.dart diff --git a/lib/blocs/explore/cubit/explore_cubit.dart b/lib/blocs/explore/cubit/explore_cubit.dart new file mode 100644 index 0000000..c7af756 --- /dev/null +++ b/lib/blocs/explore/cubit/explore_cubit.dart @@ -0,0 +1,16 @@ +import 'package:Bloomee/repository/Youtube/yt_charts_home.dart'; +import 'package:bloc/bloc.dart'; + +part 'explore_state.dart'; + +class ExploreCubit extends Cubit { + ExploreCubit() : super(ExploreInitial()) { + getTrendingVideos(); + } + + void getTrendingVideos() async { + final ytCharts = await fetchTrendingVideos(); + + emit(state.copyWith(ytCharts: ytCharts)); + } +} diff --git a/lib/blocs/explore/cubit/explore_state.dart b/lib/blocs/explore/cubit/explore_state.dart new file mode 100644 index 0000000..66846a4 --- /dev/null +++ b/lib/blocs/explore/cubit/explore_state.dart @@ -0,0 +1,21 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +part of 'explore_cubit.dart'; + +class ExploreState { + List>> ytCharts = List.empty(growable: true); + ExploreState({ + required this.ytCharts, + }); + + ExploreState copyWith({ + List>>? ytCharts, + }) { + return ExploreState( + ytCharts: ytCharts ?? this.ytCharts, + ); + } +} + +final class ExploreInitial extends ExploreState { + ExploreInitial() : super(ytCharts: []); +} diff --git a/lib/model/yt_music_model.dart b/lib/model/yt_music_model.dart index 36a1f1c..d2dfd63 100644 --- a/lib/model/yt_music_model.dart +++ b/lib/model/yt_music_model.dart @@ -10,7 +10,6 @@ MediaItemModel fromYtSongMap2MediaItem(Map songItem) { artistsID.add(element["id"]); }); artists = _artists.join(','); - print(songItem["duration"]); return MediaItemModel( id: songItem["id"] ?? 'Unknown', title: songItem["title"] ?? 'Unknown', diff --git a/lib/repository/Youtube/yt_charts_home.dart b/lib/repository/Youtube/yt_charts_home.dart new file mode 100644 index 0000000..86ae271 --- /dev/null +++ b/lib/repository/Youtube/yt_charts_home.dart @@ -0,0 +1,80 @@ +import 'package:http/http.dart' as http; +import 'dart:convert'; + +Future>>> fetchTrendingVideos() async { + // Fetch the YouTube page to extract the INNERTUBE_API_KEY + var response = await http + .get(Uri.parse('https://charts.youtube.com/charts/TrendingVideos/gb')); + final keyRegex = RegExp(r'"INNERTUBE_API_KEY"\s*:\s*"(.*?)"'); + final apiKey = keyRegex.firstMatch(response.body)?.group(1); + + if (apiKey == null) { + throw Exception('Failed to extract INNERTUBE_API_KEY'); + } + + // Prepare the headers and data for the POST request + final headers = { + 'referer': 'https://charts.youtube.com/charts/TrendingVideos/gb', + }; + + final data = { + "context": { + "client": { + "clientName": "WEB_MUSIC_ANALYTICS", + "clientVersion": "2.0", + "hl": "en", + "gl": "AR", + "experimentIds": [], + "experimentsToken": "", + "theme": "MUSIC" + }, + "capabilities": {}, + "request": {"internalExperimentFlags": []} + }, + "browseId": "FEmusic_analytics_charts_home", + "query": "perspective=CHART_HOME&chart_params_country_code=global" + }; + + // Make the POST request + response = await http.post( + Uri.parse( + 'https://charts.youtube.com/youtubei/v1/browse?alt=json&key=$apiKey'), + headers: headers, + body: json.encode(data), + ); + + if (response.statusCode == 200) { + // Parse the JSON response + // return json.decode(response.body); + List data = json.decode(response.body)["contents"] + ['sectionListRenderer']["contents"][0] + ['musicAnalyticsSectionRenderer']['content']['videos']; + + List>> playlists = []; + + for (var types in data) { + List> playlist = []; + for (var i in types['videoViews']) { + String title = i['title']; + String views = i['viewCount']; + String id = i['id']; + String img = i['thumbnail']['thumbnails'][0]['url']; + List artists = []; + for (var artist in i['artists']) { + artists.add(artist['name']); + } + playlist.add({ + 'title': title, + 'views': views, + 'id': id, + 'img': img, + 'artists': artists + }); + } + playlists.add(playlist); + } + return playlists; + } else { + throw Exception('Failed to load data: ${response.statusCode}'); + } +} diff --git a/lib/repository/Youtube/yt_music_api.dart b/lib/repository/Youtube/yt_music_api.dart index 71120f0..61733d7 100644 --- a/lib/repository/Youtube/yt_music_api.dart +++ b/lib/repository/Youtube/yt_music_api.dart @@ -260,14 +260,14 @@ class YtMusicService { final List result = data['contents']['twoColumnBrowseResultsRenderer'] ['tabs'][0]['tabRenderer']['content']['sectionListRenderer'] ['contents'] as List; - + // dev.log("result: $result", name: "YTM"); final List headResult = data['header']['carouselHeaderRenderer'] ['contents'][0]['carouselItemRenderer']['carouselItems'] as List; final List shelfRenderer = result.map((element) { return element['itemSectionRenderer']['contents'][0]['shelfRenderer']; }).toList(); - + // dev.log("${shelfRenderer.toString()}"); final List finalResult = []; for (Map element in shelfRenderer) { @@ -287,6 +287,7 @@ class YtMusicService { name: "YTM"); } } + // dev.log("finalResult: $finalResult", name: "YTM"); final List finalHeadResult = formatHeadItems(headResult); finalResult.removeWhere((element) => element == null); @@ -882,7 +883,7 @@ class YtMusicService { }; // pprint(cToken); final response = await sendRequest(endpoint, body, headers, params: params); - print(response); + // print(response); } Future getAlbumDetails(String albumId) async { diff --git a/lib/repository/Youtube/yt_music_home.dart b/lib/repository/Youtube/yt_music_home.dart new file mode 100644 index 0000000..ab360d8 --- /dev/null +++ b/lib/repository/Youtube/yt_music_home.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'package:http/http.dart' as http; + +String decodeEscapeSequences(String encodedString) { + return encodedString.replaceAllMapped(RegExp(r'\\x([0-9a-fA-F]{2})'), + (match) => String.fromCharCode(int.parse(match.group(1)!, radix: 16))); +} + +Future> fetchMusicData() async { + final client = http.Client(); + final headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + 'Sec-Ch-Ua': + '"Chromium";v="94", "Google Chrome";v="94", ";Not A Brand";v="99"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + }; + + final response = await client.get(Uri.parse('https://music.youtube.com/'), + headers: headers); + if (response.statusCode == 200) { + final html = response.body; + final pattern = RegExp(r"data:\s*'\\x7b(.*?)'"); + final matches = pattern.allMatches(html); + if (matches.isNotEmpty) { + final encodedString = + r'\x7b'.toString() + matches.first.group(1).toString(); + final decodedBytes = + utf8.decode(decodeEscapeSequences(encodedString).codeUnits); + + final Map data = jsonDecode(decodedBytes); + // log(data.keys.toString(), name: "YT Music Home Data Keys"); + final items = []; + final contents = data['contents']['singleColumnBrowseResultsRenderer'] + ['tabs'][0]['tabRenderer']['content']['sectionListRenderer'] + ['contents'][0]['musicCarouselShelfRenderer']['contents']; + + for (var content in contents) { + final img = content['musicResponsiveListItemRenderer']['thumbnail'] + ['musicThumbnailRenderer']["thumbnail"]["thumbnails"][0]["url"]; + final title = content['musicResponsiveListItemRenderer']['flexColumns'] + [0]['musicResponsiveListItemFlexColumnRenderer']["text"]["runs"] + [0]["text"]; + final watchid = content['musicResponsiveListItemRenderer'] + ['flexColumns'][0] + ['musicResponsiveListItemFlexColumnRenderer']["text"]["runs"][0] + ["navigationEndpoint"]["watchEndpoint"]["videoId"]; + final playlistid = content['musicResponsiveListItemRenderer'] + ['flexColumns'][0] + ['musicResponsiveListItemFlexColumnRenderer']["text"]["runs"][0] + ["navigationEndpoint"]["watchEndpoint"]["playlistId"]; + var artists = ''; + for (var artist in content['musicResponsiveListItemRenderer'] + ['flexColumns'][1]['musicResponsiveListItemFlexColumnRenderer'] + ["text"]["runs"]) { + artists += artist["text"]; + } + items.add({ + "title": title, + "img": img, + "watchid": watchid, + "playlistid": playlistid, + "artists": artists + }); + } + + return items; + } + } + + throw Exception('Failed to load'); +} diff --git a/lib/repository/Youtube/ytmusic_format.dart b/lib/repository/Youtube/ytmusic_format.dart index 5a2d104..24c00d9 100644 --- a/lib/repository/Youtube/ytmusic_format.dart +++ b/lib/repository/Youtube/ytmusic_format.dart @@ -144,9 +144,9 @@ Future formatHomeSections(List items) async { 'count': e['compactStationRenderer']['videoCountText']['runs'][0] ['text'], 'id': - 'youtube${e['compactStationRenderer']['navigationEndpoint']['watchEndpoint']['playlistId']}', + 'youtube${e['compactStationRenderer']['navigationEndpoint']['watchPlaylistEndpoint']['playlistId']}', 'firstItemId': - 'youtube${e['compactStationRenderer']['navigationEndpoint']['watchEndpoint']['videoId']}', + 'youtube${e['compactStationRenderer']['navigationEndpoint']['watchPlaylistEndpoint']['videoId']}', 'image': e['compactStationRenderer']['thumbnail']['thumbnails'][0] ['url'], 'images': [ diff --git a/lib/screens/screen/chart/chart_view.dart b/lib/screens/screen/chart/chart_view.dart index 0f8fe7d..c3d6aa1 100644 --- a/lib/screens/screen/chart/chart_view.dart +++ b/lib/screens/screen/chart/chart_view.dart @@ -53,6 +53,7 @@ class _ChartScreenState extends State { } else { final List>? melon = snapshot.data; return CustomScrollView( + physics: const BouncingScrollPhysics(), slivers: [ customDiscoverBar(context), //AppBar SliverList( diff --git a/lib/screens/screen/explore_screen.dart b/lib/screens/screen/explore_screen.dart index ad3a1d5..6041e8c 100644 --- a/lib/screens/screen/explore_screen.dart +++ b/lib/screens/screen/explore_screen.dart @@ -1,3 +1,4 @@ +import 'package:Bloomee/blocs/explore/cubit/explore_cubit.dart'; import 'package:Bloomee/services/db/cubit/bloomee_db_cubit.dart'; import 'package:Bloomee/utils/app_updater.dart'; import 'package:flutter/material.dart'; @@ -33,25 +34,29 @@ class _ExploreScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: CustomScrollView( - physics: const BouncingScrollPhysics(), - slivers: [ - customDiscoverBar(context), //AppBar - SliverList( - delegate: SliverChildListDelegate([ - Padding( - padding: const EdgeInsets.only(top: 20), - child: CaraouselWidget(), - ), - const Padding( - padding: EdgeInsets.only(top: 20), - child: TabSongListWidget(), - ), - ])) - ], + return BlocProvider( + create: (context) => ExploreCubit(), + lazy: false, + child: Scaffold( + body: CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + customDiscoverBar(context), //AppBar + SliverList( + delegate: SliverChildListDelegate([ + Padding( + padding: const EdgeInsets.only(top: 20), + child: CaraouselWidget(), + ), + const Padding( + padding: EdgeInsets.only(top: 20), + child: TabSongListWidget(), + ), + ])) + ], + ), + backgroundColor: Default_Theme.themeColor, ), - backgroundColor: Default_Theme.themeColor, ); } diff --git a/lib/screens/widgets/chart_list_tile.dart b/lib/screens/widgets/chart_list_tile.dart index af9fb3c..93e91e9 100644 --- a/lib/screens/widgets/chart_list_tile.dart +++ b/lib/screens/widgets/chart_list_tile.dart @@ -22,25 +22,30 @@ class ChartListTile extends StatelessWidget { return InkWell( onTap: () => context.push( "/${GlobalStrConsts.searchScreen}?query=${title} by ${subtitle}"), - child: ListTile( - leading: loadImageCached(imgUrl), - title: Text( - title, - textAlign: TextAlign.start, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: Default_Theme.tertiaryTextStyle.merge(const TextStyle( - fontWeight: FontWeight.w600, - color: Default_Theme.primaryColor1, - fontSize: 14)), - ), - subtitle: Text(subtitle, + child: SizedBox( + width: 300, + child: ListTile( + leading: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox(height: 60, child: loadImageCached(imgUrl))), + title: Text( + title, textAlign: TextAlign.start, overflow: TextOverflow.ellipsis, maxLines: 1, - style: Default_Theme.tertiaryTextStyle.merge(TextStyle( - color: Default_Theme.primaryColor1.withOpacity(0.8), - fontSize: 13))), + style: Default_Theme.tertiaryTextStyle.merge(const TextStyle( + fontWeight: FontWeight.w600, + color: Default_Theme.primaryColor1, + fontSize: 14)), + ), + subtitle: Text(subtitle, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: Default_Theme.tertiaryTextStyle.merge(TextStyle( + color: Default_Theme.primaryColor1.withOpacity(0.8), + fontSize: 13))), + ), ), ); } diff --git a/lib/screens/widgets/tabList_widget.dart b/lib/screens/widgets/tabList_widget.dart index 5e9043a..8035f33 100644 --- a/lib/screens/widgets/tabList_widget.dart +++ b/lib/screens/widgets/tabList_widget.dart @@ -1,7 +1,8 @@ +import 'package:Bloomee/blocs/explore/cubit/explore_cubit.dart'; +import 'package:Bloomee/screens/widgets/chart_list_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:Bloomee/repository/Saavn/cubit/saavn_repository_cubit.dart'; -import 'package:Bloomee/screens/widgets/horizontalSongCard_widget.dart'; import '../../theme_data/default.dart'; @@ -110,16 +111,9 @@ class SongListWidget extends StatelessWidget { child: SizedBox( // height: MediaQuery.of(context).size.height * 0.46, // width: MediaQuery.of(context).size.width * 0.82, - child: BlocBuilder( - buildWhen: (previous, current) { - if (current.albumName == "Trendings" && previous != current) { - return true; - } else { - return false; - } - }, + child: BlocBuilder( builder: (context, state) { - if (state is SaavnRepositoryInitial) { + if (state is ExploreInitial) { return const Center( child: Padding( padding: EdgeInsets.only(right: 70), @@ -132,21 +126,15 @@ class SongListWidget extends StatelessWidget { ), ); } else { - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.mediaItems.length, - padding: EdgeInsets.zero, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(bottom: 5), - child: HorizontalSongCardWidget( - mediaPlaylist: state, - index: index, - showLiked: true, - showOptions: true, - )); - }); + List _list = state.ytCharts[0].map((e) { + return ChartListTile( + title: e["title"], + subtitle: e["artists"][0], + imgUrl: e["img"], + ); + }).toList(); + + return buildTrendingCards(_list); } }, ), @@ -154,3 +142,38 @@ class SongListWidget extends StatelessWidget { ); } } + +Widget buildTrendingCards(List product) { + final cards = []; + Widget feautredCards; + int endIndex = 4; + if (endIndex > product.length) endIndex = product.length; + if (product.isNotEmpty) { + for (int i = 0; i < product.length; i += 4) { + if (endIndex > product.length) endIndex = product.length; + List currentRow = product.sublist(i, endIndex); + endIndex = i + 8; + cards.add(Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: currentRow, + )); + } + feautredCards = Container( + padding: EdgeInsets.only(top: 16, bottom: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SingleChildScrollView( + physics: BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + child: Row(mainAxisSize: MainAxisSize.min, children: cards), + ), + ], + ), + ); + } else { + feautredCards = Container(); + } + return feautredCards; +} diff --git a/pubspec.yaml b/pubspec.yaml index b6f8058..efc4ddc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: flutter_bloc: ^8.1.3 dart_des: ^1.0.2 http: ^1.1.0 + convert: ^3.1.1 html: ^0.15.4 logging: ^1.2.0 google_nav_bar: ^5.0.6