mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: add Spotify homepage personalized recommendations (#1402)
* feat: add spotify homepage recommendations * chore: bring back made for user sectin
This commit is contained in:
parent
9791e3fb5f
commit
9e25c742d4
@ -88,7 +88,7 @@ class Assets {
|
|||||||
AssetGenImage('assets/user-placeholder.png');
|
AssetGenImage('assets/user-placeholder.png');
|
||||||
|
|
||||||
/// List of all assets
|
/// List of all assets
|
||||||
List<dynamic> get values => [
|
static List<dynamic> get values => [
|
||||||
albumPlaceholder,
|
albumPlaceholder,
|
||||||
bengaliPatternsBg,
|
bengaliPatternsBg,
|
||||||
branding,
|
branding,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
import 'package:spotube/extensions/track.dart';
|
||||||
|
import 'package:spotube/models/spotify/home_feed.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
|
|
||||||
abstract class FakeData {
|
abstract class FakeData {
|
||||||
@ -196,4 +197,30 @@ abstract class FakeData {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static final feedSection = SpotifyHomeFeedSection(
|
||||||
|
typename: "HomeGenericSectionData",
|
||||||
|
uri: "spotify:section:lol",
|
||||||
|
title: "Dummy",
|
||||||
|
items: [
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
SpotifyHomeFeedSectionItem(
|
||||||
|
typename: "PlaylistResponseWrapper",
|
||||||
|
playlist: SpotifySectionPlaylist(
|
||||||
|
name: "Playlist $i",
|
||||||
|
description: "Really super important description $i",
|
||||||
|
format: "daily-mix",
|
||||||
|
images: [
|
||||||
|
const SpotifySectionItemImage(
|
||||||
|
height: 1,
|
||||||
|
width: 1,
|
||||||
|
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
owner: "Spotify",
|
||||||
|
uri: "spotify:playlist:id",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import 'package:spotube/pages/album/album.dart';
|
|||||||
import 'package:spotube/pages/connect/connect.dart';
|
import 'package:spotube/pages/connect/connect.dart';
|
||||||
import 'package:spotube/pages/connect/control/control.dart';
|
import 'package:spotube/pages/connect/control/control.dart';
|
||||||
import 'package:spotube/pages/getting_started/getting_started.dart';
|
import 'package:spotube/pages/getting_started/getting_started.dart';
|
||||||
|
import 'package:spotube/pages/home/feed/feed_section.dart';
|
||||||
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
||||||
import 'package:spotube/pages/home/genres/genres.dart';
|
import 'package:spotube/pages/home/genres/genres.dart';
|
||||||
import 'package:spotube/pages/home/home.dart';
|
import 'package:spotube/pages/home/home.dart';
|
||||||
@ -76,6 +77,14 @@ final routerProvider = Provider((ref) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "feeds/:feedId",
|
||||||
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
|
child: HomeFeedSectionPage(
|
||||||
|
sectionUri: state.pathParameters["feedId"] as String,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
52
lib/components/home/sections/feed.dart
Normal file
52
lib/components/home/sections/feed.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
|
import 'package:spotube/provider/spotify/views/home.dart';
|
||||||
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
|
class HomePageFeedSection extends HookConsumerWidget {
|
||||||
|
const HomePageFeedSection({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final homeFeed = ref.watch(homeViewProvider);
|
||||||
|
final nonShortSections = homeFeed.asData?.value?.sections
|
||||||
|
.where((s) => s.typename == "HomeGenericSectionData")
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
return SliverList.builder(
|
||||||
|
itemCount: nonShortSections.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final section = nonShortSections[index];
|
||||||
|
if (section.items.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return HorizontalPlaybuttonCardView(
|
||||||
|
items: [
|
||||||
|
for (final item in section.items)
|
||||||
|
if (item.album != null)
|
||||||
|
item.album!.asAlbum
|
||||||
|
else if (item.artist != null)
|
||||||
|
item.artist!.asArtist
|
||||||
|
else if (item.playlist != null)
|
||||||
|
item.playlist!.asPlaylist
|
||||||
|
],
|
||||||
|
title: Text(section.title ?? "No Titel"),
|
||||||
|
hasNextPage: false,
|
||||||
|
isLoadingNextPage: false,
|
||||||
|
onFetchMore: () {},
|
||||||
|
titleTrailing: Directionality(
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
child: TextButton.icon(
|
||||||
|
label: const Text("Browse More"),
|
||||||
|
icon: const Icon(SpotubeIcons.angleRight),
|
||||||
|
onPressed: () =>
|
||||||
|
ServiceUtils.push(context, "/feeds/${section.uri}"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -17,18 +17,21 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
final VoidCallback onFetchMore;
|
final VoidCallback onFetchMore;
|
||||||
final bool isLoadingNextPage;
|
final bool isLoadingNextPage;
|
||||||
final bool hasNextPage;
|
final bool hasNextPage;
|
||||||
|
final Widget? titleTrailing;
|
||||||
|
|
||||||
const HorizontalPlaybuttonCardView({
|
HorizontalPlaybuttonCardView({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.items,
|
required this.items,
|
||||||
required this.hasNextPage,
|
required this.hasNextPage,
|
||||||
required this.onFetchMore,
|
required this.onFetchMore,
|
||||||
required this.isLoadingNextPage,
|
required this.isLoadingNextPage,
|
||||||
|
this.titleTrailing,
|
||||||
super.key,
|
super.key,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
items is List<PlaylistSimple> ||
|
items.every(
|
||||||
items is List<Album> ||
|
(item) =>
|
||||||
items is List<Artist>,
|
item is PlaylistSimple || item is Artist || item is AlbumSimple,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -48,9 +51,15 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
DefaultTextStyle(
|
Row(
|
||||||
style: textTheme.titleMedium!,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
child: title,
|
children: [
|
||||||
|
DefaultTextStyle(
|
||||||
|
style: textTheme.titleMedium!,
|
||||||
|
child: title,
|
||||||
|
),
|
||||||
|
if (titleTrailing != null) titleTrailing!,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: height,
|
height: height,
|
||||||
@ -87,7 +96,7 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
|
|||||||
return switch (item) {
|
return switch (item) {
|
||||||
PlaylistSimple() =>
|
PlaylistSimple() =>
|
||||||
PlaylistCard(item as PlaylistSimple),
|
PlaylistCard(item as PlaylistSimple),
|
||||||
Album() => AlbumCard(item as Album),
|
AlbumSimple() => AlbumCard(item as Album),
|
||||||
Artist() => Padding(
|
Artist() => Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12.0),
|
horizontal: 12.0),
|
||||||
|
@ -39,6 +39,7 @@ import 'package:spotube/hooks/configurators/use_init_sys_tray.dart';
|
|||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
|
import 'package:timezone/data/latest.dart' as tz;
|
||||||
|
|
||||||
Future<void> main(List<String> rawArgs) async {
|
Future<void> main(List<String> rawArgs) async {
|
||||||
final arguments = await startCLI(rawArgs);
|
final arguments = await startCLI(rawArgs);
|
||||||
@ -47,6 +48,8 @@ Future<void> main(List<String> rawArgs) async {
|
|||||||
|
|
||||||
await registerWindowsScheme("spotify");
|
await registerWindowsScheme("spotify");
|
||||||
|
|
||||||
|
tz.initializeTimeZones();
|
||||||
|
|
||||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||||
|
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
|
247
lib/models/spotify/home_feed.dart
Normal file
247
lib/models/spotify/home_feed.dart
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
|
part 'home_feed.freezed.dart';
|
||||||
|
part 'home_feed.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SpotifySectionPlaylist with _$SpotifySectionPlaylist {
|
||||||
|
const SpotifySectionPlaylist._();
|
||||||
|
|
||||||
|
const factory SpotifySectionPlaylist({
|
||||||
|
required String description,
|
||||||
|
required String format,
|
||||||
|
required List<SpotifySectionItemImage> images,
|
||||||
|
required String name,
|
||||||
|
required String owner,
|
||||||
|
required String uri,
|
||||||
|
}) = _SpotifySectionPlaylist;
|
||||||
|
|
||||||
|
factory SpotifySectionPlaylist.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SpotifySectionPlaylistFromJson(json);
|
||||||
|
|
||||||
|
String get id => uri.split(":").last;
|
||||||
|
|
||||||
|
Playlist get asPlaylist {
|
||||||
|
return Playlist()
|
||||||
|
..id = id
|
||||||
|
..name = name
|
||||||
|
..description = description
|
||||||
|
..collaborative = false
|
||||||
|
..images = images.map((e) => e.asImage).toList()
|
||||||
|
..owner = (User()..displayName = "Spotify")
|
||||||
|
..uri = uri
|
||||||
|
..type = "playlist";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SpotifySectionArtist with _$SpotifySectionArtist {
|
||||||
|
const SpotifySectionArtist._();
|
||||||
|
|
||||||
|
const factory SpotifySectionArtist({
|
||||||
|
required String name,
|
||||||
|
required String uri,
|
||||||
|
required List<SpotifySectionItemImage> images,
|
||||||
|
}) = _SpotifySectionArtist;
|
||||||
|
|
||||||
|
factory SpotifySectionArtist.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SpotifySectionArtistFromJson(json);
|
||||||
|
|
||||||
|
String get id => uri.split(":").last;
|
||||||
|
|
||||||
|
Artist get asArtist {
|
||||||
|
return Artist()
|
||||||
|
..id = id
|
||||||
|
..name = name
|
||||||
|
..images = images.map((e) => e.asImage).toList()
|
||||||
|
..type = "artist"
|
||||||
|
..uri = uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SpotifySectionAlbum with _$SpotifySectionAlbum {
|
||||||
|
const SpotifySectionAlbum._();
|
||||||
|
|
||||||
|
const factory SpotifySectionAlbum({
|
||||||
|
required List<SpotifySectionAlbumArtist> artists,
|
||||||
|
required List<SpotifySectionItemImage> images,
|
||||||
|
required String name,
|
||||||
|
required String uri,
|
||||||
|
}) = _SpotifySectionAlbum;
|
||||||
|
|
||||||
|
factory SpotifySectionAlbum.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SpotifySectionAlbumFromJson(json);
|
||||||
|
|
||||||
|
String get id => uri.split(":").last;
|
||||||
|
|
||||||
|
Album get asAlbum {
|
||||||
|
return Album()
|
||||||
|
..id = id
|
||||||
|
..name = name
|
||||||
|
..artists = artists.map((a) => a.asArtist).toList()
|
||||||
|
..albumType = AlbumType.album
|
||||||
|
..images = images.map((e) => e.asImage).toList()
|
||||||
|
..uri = uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SpotifySectionAlbumArtist with _$SpotifySectionAlbumArtist {
|
||||||
|
const SpotifySectionAlbumArtist._();
|
||||||
|
|
||||||
|
const factory SpotifySectionAlbumArtist({
|
||||||
|
required String name,
|
||||||
|
required String uri,
|
||||||
|
}) = _SpotifySectionAlbumArtist;
|
||||||
|
|
||||||
|
factory SpotifySectionAlbumArtist.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SpotifySectionAlbumArtistFromJson(json);
|
||||||
|
|
||||||
|
String get id => uri.split(":").last;
|
||||||
|
|
||||||
|
Artist get asArtist {
|
||||||
|
return Artist()
|
||||||
|
..id = id
|
||||||
|
..name = name
|
||||||
|
..type = "artist"
|
||||||
|
..uri = uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SpotifySectionItemImage with _$SpotifySectionItemImage {
|
||||||
|
const SpotifySectionItemImage._();
|
||||||
|
|
||||||
|
const factory SpotifySectionItemImage({
|
||||||
|
required num? height,
|
||||||
|
required String url,
|
||||||
|
required num? width,
|
||||||
|
}) = _SpotifySectionItemImage;
|
||||||
|
|
||||||
|
factory SpotifySectionItemImage.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SpotifySectionItemImageFromJson(json);
|
||||||
|
|
||||||
|
Image get asImage {
|
||||||
|
return Image()
|
||||||
|
..height = height?.toInt()
|
||||||
|
..width = width?.toInt()
|
||||||
|
..url = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SpotifyHomeFeedSectionItem with _$SpotifyHomeFeedSectionItem {
|
||||||
|
factory SpotifyHomeFeedSectionItem({
|
||||||
|
required String typename,
|
||||||
|
SpotifySectionPlaylist? playlist,
|
||||||
|
SpotifySectionArtist? artist,
|
||||||
|
SpotifySectionAlbum? album,
|
||||||
|
}) = _SpotifyHomeFeedSectionItem;
|
||||||
|
|
||||||
|
factory SpotifyHomeFeedSectionItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SpotifyHomeFeedSectionItemFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SpotifyHomeFeedSection with _$SpotifyHomeFeedSection {
|
||||||
|
factory SpotifyHomeFeedSection({
|
||||||
|
required String typename,
|
||||||
|
String? title,
|
||||||
|
required String uri,
|
||||||
|
required List<SpotifyHomeFeedSectionItem> items,
|
||||||
|
}) = _SpotifyHomeFeedSection;
|
||||||
|
|
||||||
|
factory SpotifyHomeFeedSection.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SpotifyHomeFeedSectionFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SpotifyHomeFeed with _$SpotifyHomeFeed {
|
||||||
|
factory SpotifyHomeFeed({
|
||||||
|
required String greeting,
|
||||||
|
required List<SpotifyHomeFeedSection> sections,
|
||||||
|
}) = _SpotifyHomeFeed;
|
||||||
|
|
||||||
|
factory SpotifyHomeFeed.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SpotifyHomeFeedFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> transformSectionItemTypeJsonMap(
|
||||||
|
Map<String, dynamic> json) {
|
||||||
|
final data = json["content"]["data"];
|
||||||
|
final objType = json["content"]["data"]["__typename"];
|
||||||
|
return {
|
||||||
|
"typename": json["content"]["__typename"],
|
||||||
|
if (objType == "Playlist")
|
||||||
|
"playlist": {
|
||||||
|
"name": data["name"],
|
||||||
|
"description": data["description"],
|
||||||
|
"format": data["format"],
|
||||||
|
"images": (data["images"]["items"] as List)
|
||||||
|
.expand((j) => j["sources"] as dynamic)
|
||||||
|
.toList()
|
||||||
|
.cast<Map<String, dynamic>>(),
|
||||||
|
"owner": data["ownerV2"]["data"]["name"],
|
||||||
|
"uri": data["uri"]
|
||||||
|
},
|
||||||
|
if (objType == "Artist")
|
||||||
|
"artist": {
|
||||||
|
"name": data["profile"]["name"],
|
||||||
|
"uri": data["uri"],
|
||||||
|
"images": data["visuals"]["avatarImage"]["sources"],
|
||||||
|
},
|
||||||
|
if (objType == "Album")
|
||||||
|
"album": {
|
||||||
|
"name": data["name"],
|
||||||
|
"uri": data["uri"],
|
||||||
|
"images": data["coverArt"]["sources"],
|
||||||
|
"artists": data["artists"]["items"]
|
||||||
|
.map(
|
||||||
|
(artist) => {
|
||||||
|
"name": artist["profile"]["name"],
|
||||||
|
"uri": artist["uri"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> transformSectionItemJsonMap(Map<String, dynamic> json) {
|
||||||
|
return {
|
||||||
|
"typename": json["data"]["__typename"],
|
||||||
|
"title": json["data"]?["title"]?["text"],
|
||||||
|
"uri": json["uri"],
|
||||||
|
"items": (json["sectionItems"]["items"] as List)
|
||||||
|
.map(
|
||||||
|
(data) =>
|
||||||
|
transformSectionItemTypeJsonMap(data as Map<String, dynamic>)
|
||||||
|
as dynamic,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(w) =>
|
||||||
|
w["playlist"] != null ||
|
||||||
|
w["artist"] != null ||
|
||||||
|
w["album"] != null,
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
.cast<Map<String, dynamic>>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> transformHomeFeedJsonMap(Map<String, dynamic> json) {
|
||||||
|
return {
|
||||||
|
"greeting": json["data"]["home"]["greeting"]["text"],
|
||||||
|
"sections":
|
||||||
|
(json["data"]["home"]["sectionContainer"]["sections"]["items"] as List)
|
||||||
|
.map(
|
||||||
|
(item) =>
|
||||||
|
transformSectionItemJsonMap(item as Map<String, dynamic>)
|
||||||
|
as dynamic,
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
.cast<Map<String, dynamic>>()
|
||||||
|
};
|
||||||
|
}
|
1666
lib/models/spotify/home_feed.freezed.dart
Normal file
1666
lib/models/spotify/home_feed.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
169
lib/models/spotify/home_feed.g.dart
Normal file
169
lib/models/spotify/home_feed.g.dart
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'home_feed.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$SpotifySectionPlaylistImpl _$$SpotifySectionPlaylistImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SpotifySectionPlaylistImpl(
|
||||||
|
description: json['description'] as String,
|
||||||
|
format: json['format'] as String,
|
||||||
|
images: (json['images'] as List<dynamic>)
|
||||||
|
.map((e) =>
|
||||||
|
SpotifySectionItemImage.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
name: json['name'] as String,
|
||||||
|
owner: json['owner'] as String,
|
||||||
|
uri: json['uri'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SpotifySectionPlaylistImplToJson(
|
||||||
|
_$SpotifySectionPlaylistImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'description': instance.description,
|
||||||
|
'format': instance.format,
|
||||||
|
'images': instance.images,
|
||||||
|
'name': instance.name,
|
||||||
|
'owner': instance.owner,
|
||||||
|
'uri': instance.uri,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SpotifySectionArtistImpl _$$SpotifySectionArtistImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SpotifySectionArtistImpl(
|
||||||
|
name: json['name'] as String,
|
||||||
|
uri: json['uri'] as String,
|
||||||
|
images: (json['images'] as List<dynamic>)
|
||||||
|
.map((e) =>
|
||||||
|
SpotifySectionItemImage.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SpotifySectionArtistImplToJson(
|
||||||
|
_$SpotifySectionArtistImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'name': instance.name,
|
||||||
|
'uri': instance.uri,
|
||||||
|
'images': instance.images,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SpotifySectionAlbumImpl _$$SpotifySectionAlbumImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SpotifySectionAlbumImpl(
|
||||||
|
artists: (json['artists'] as List<dynamic>)
|
||||||
|
.map((e) =>
|
||||||
|
SpotifySectionAlbumArtist.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
images: (json['images'] as List<dynamic>)
|
||||||
|
.map((e) =>
|
||||||
|
SpotifySectionItemImage.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
name: json['name'] as String,
|
||||||
|
uri: json['uri'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SpotifySectionAlbumImplToJson(
|
||||||
|
_$SpotifySectionAlbumImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'artists': instance.artists,
|
||||||
|
'images': instance.images,
|
||||||
|
'name': instance.name,
|
||||||
|
'uri': instance.uri,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SpotifySectionAlbumArtistImpl _$$SpotifySectionAlbumArtistImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SpotifySectionAlbumArtistImpl(
|
||||||
|
name: json['name'] as String,
|
||||||
|
uri: json['uri'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SpotifySectionAlbumArtistImplToJson(
|
||||||
|
_$SpotifySectionAlbumArtistImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'name': instance.name,
|
||||||
|
'uri': instance.uri,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SpotifySectionItemImageImpl _$$SpotifySectionItemImageImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SpotifySectionItemImageImpl(
|
||||||
|
height: json['height'] as num?,
|
||||||
|
url: json['url'] as String,
|
||||||
|
width: json['width'] as num?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SpotifySectionItemImageImplToJson(
|
||||||
|
_$SpotifySectionItemImageImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'height': instance.height,
|
||||||
|
'url': instance.url,
|
||||||
|
'width': instance.width,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SpotifyHomeFeedSectionItemImpl _$$SpotifyHomeFeedSectionItemImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SpotifyHomeFeedSectionItemImpl(
|
||||||
|
typename: json['typename'] as String,
|
||||||
|
playlist: json['playlist'] == null
|
||||||
|
? null
|
||||||
|
: SpotifySectionPlaylist.fromJson(
|
||||||
|
json['playlist'] as Map<String, dynamic>),
|
||||||
|
artist: json['artist'] == null
|
||||||
|
? null
|
||||||
|
: SpotifySectionArtist.fromJson(
|
||||||
|
json['artist'] as Map<String, dynamic>),
|
||||||
|
album: json['album'] == null
|
||||||
|
? null
|
||||||
|
: SpotifySectionAlbum.fromJson(json['album'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SpotifyHomeFeedSectionItemImplToJson(
|
||||||
|
_$SpotifyHomeFeedSectionItemImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'typename': instance.typename,
|
||||||
|
'playlist': instance.playlist,
|
||||||
|
'artist': instance.artist,
|
||||||
|
'album': instance.album,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SpotifyHomeFeedSectionImpl _$$SpotifyHomeFeedSectionImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SpotifyHomeFeedSectionImpl(
|
||||||
|
typename: json['typename'] as String,
|
||||||
|
title: json['title'] as String?,
|
||||||
|
uri: json['uri'] as String,
|
||||||
|
items: (json['items'] as List<dynamic>)
|
||||||
|
.map((e) =>
|
||||||
|
SpotifyHomeFeedSectionItem.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SpotifyHomeFeedSectionImplToJson(
|
||||||
|
_$SpotifyHomeFeedSectionImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'typename': instance.typename,
|
||||||
|
'title': instance.title,
|
||||||
|
'uri': instance.uri,
|
||||||
|
'items': instance.items,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SpotifyHomeFeedImpl _$$SpotifyHomeFeedImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SpotifyHomeFeedImpl(
|
||||||
|
greeting: json['greeting'] as String,
|
||||||
|
sections: (json['sections'] as List<dynamic>)
|
||||||
|
.map(
|
||||||
|
(e) => SpotifyHomeFeedSection.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SpotifyHomeFeedImplToJson(
|
||||||
|
_$SpotifyHomeFeedImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'greeting': instance.greeting,
|
||||||
|
'sections': instance.sections,
|
||||||
|
};
|
62
lib/pages/home/feed/feed_section.dart
Normal file
62
lib/pages/home/feed/feed_section.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:spotube/collections/fake.dart';
|
||||||
|
import 'package:spotube/components/album/album_card.dart';
|
||||||
|
import 'package:spotube/components/artist/artist_card.dart';
|
||||||
|
import 'package:spotube/components/playlist/playlist_card.dart';
|
||||||
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
import 'package:spotube/provider/spotify/views/home_section.dart';
|
||||||
|
|
||||||
|
class HomeFeedSectionPage extends HookConsumerWidget {
|
||||||
|
final String sectionUri;
|
||||||
|
const HomeFeedSectionPage({super.key, required this.sectionUri});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final homeFeedSection = ref.watch(homeSectionViewProvider(sectionUri));
|
||||||
|
final section = homeFeedSection.asData?.value ?? FakeData.feedSection;
|
||||||
|
|
||||||
|
return Skeletonizer(
|
||||||
|
enabled: homeFeedSection.isLoading,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: PageWindowTitleBar(
|
||||||
|
title: Text(section.title ?? ""),
|
||||||
|
centerTitle: false,
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
|
titleSpacing: 0,
|
||||||
|
),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverLayoutBuilder(
|
||||||
|
builder: (context, constrains) {
|
||||||
|
return SliverGrid.builder(
|
||||||
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 200,
|
||||||
|
mainAxisExtent: constrains.smAndDown ? 225 : 250,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
),
|
||||||
|
itemCount: section.items.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = section.items[index];
|
||||||
|
|
||||||
|
if (item.album != null) {
|
||||||
|
return AlbumCard(item.album!.asAlbum);
|
||||||
|
} else if (item.artist != null) {
|
||||||
|
return ArtistCard(item.artist!.asArtist);
|
||||||
|
} else if (item.playlist != null) {
|
||||||
|
return PlaylistCard(item.playlist!.asPlaylist);
|
||||||
|
}
|
||||||
|
return const SizedBox();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/components/connect/connect_device.dart';
|
import 'package:spotube/components/connect/connect_device.dart';
|
||||||
import 'package:spotube/components/home/sections/featured.dart';
|
import 'package:spotube/components/home/sections/featured.dart';
|
||||||
|
import 'package:spotube/components/home/sections/feed.dart';
|
||||||
import 'package:spotube/components/home/sections/friends.dart';
|
import 'package:spotube/components/home/sections/friends.dart';
|
||||||
import 'package:spotube/components/home/sections/genres.dart';
|
import 'package:spotube/components/home/sections/genres.dart';
|
||||||
import 'package:spotube/components/home/sections/made_for_user.dart';
|
import 'package:spotube/components/home/sections/made_for_user.dart';
|
||||||
@ -66,6 +67,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
const SliverToBoxAdapter(child: HomeFeaturedSection()),
|
const SliverToBoxAdapter(child: HomeFeaturedSection()),
|
||||||
const HomePageFriendsSection(),
|
const HomePageFriendsSection(),
|
||||||
const SliverToBoxAdapter(child: HomeNewReleasesSection()),
|
const SliverToBoxAdapter(child: HomeNewReleasesSection()),
|
||||||
|
const HomePageFeedSection(),
|
||||||
const SliverSafeArea(sliver: HomeMadeForUserSection()),
|
const SliverSafeArea(sliver: HomeMadeForUserSection()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -12,7 +11,6 @@ class WebViewLogin extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final mounted = useIsMounted();
|
|
||||||
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
|
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
|
||||||
|
|
||||||
if (kIsDesktop) {
|
if (kIsDesktop) {
|
||||||
@ -57,7 +55,7 @@ class WebViewLogin extends HookConsumerWidget {
|
|||||||
authenticationNotifier.setCredentials(
|
authenticationNotifier.setCredentials(
|
||||||
await AuthenticationCredentials.fromCookie(cookieHeader),
|
await AuthenticationCredentials.fromCookie(cookieHeader),
|
||||||
);
|
);
|
||||||
if (mounted()) {
|
if (context.mounted) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
GoRouter.of(context).go("/");
|
GoRouter.of(context).go("/");
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
@ -25,12 +26,16 @@ class AuthenticationCredentials {
|
|||||||
|
|
||||||
static Future<AuthenticationCredentials> fromCookie(String cookie) async {
|
static Future<AuthenticationCredentials> fromCookie(String cookie) async {
|
||||||
try {
|
try {
|
||||||
|
final spDc = cookie
|
||||||
|
.split("; ")
|
||||||
|
.firstWhereOrNull((c) => c.trim().startsWith("sp_dc="))
|
||||||
|
?.trim();
|
||||||
final res = await get(
|
final res = await get(
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
"https://open.spotify.com/get_access_token?reason=transport&productType=web_player",
|
"https://open.spotify.com/get_access_token?reason=transport&productType=web_player",
|
||||||
),
|
),
|
||||||
headers: {
|
headers: {
|
||||||
"Cookie": cookie,
|
"Cookie": spDc ?? "",
|
||||||
"User-Agent":
|
"User-Agent":
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
|
||||||
},
|
},
|
||||||
@ -44,7 +49,7 @@ class AuthenticationCredentials {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return AuthenticationCredentials(
|
return AuthenticationCredentials(
|
||||||
cookie: cookie,
|
cookie: "${res.headers["set-cookie"]}; $spDc",
|
||||||
accessToken: body['accessToken'],
|
accessToken: body['accessToken'],
|
||||||
expiration: DateTime.fromMillisecondsSinceEpoch(
|
expiration: DateTime.fromMillisecondsSinceEpoch(
|
||||||
body['accessTokenExpirationTimestampMs'],
|
body['accessTokenExpirationTimestampMs'],
|
||||||
@ -64,6 +69,15 @@ class AuthenticationCredentials {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the cookie value
|
||||||
|
String? getCookie(String key) => cookie
|
||||||
|
.split("; ")
|
||||||
|
.firstWhereOrNull((c) => c.trim().startsWith("$key="))
|
||||||
|
?.trim()
|
||||||
|
.split("=")
|
||||||
|
.last
|
||||||
|
.replaceAll(";", "");
|
||||||
|
|
||||||
factory AuthenticationCredentials.fromJson(Map<String, dynamic> json) {
|
factory AuthenticationCredentials.fromJson(Map<String, dynamic> json) {
|
||||||
return AuthenticationCredentials(
|
return AuthenticationCredentials(
|
||||||
cookie: json['cookie'] as String,
|
cookie: json['cookie'] as String,
|
||||||
|
22
lib/provider/spotify/views/home.dart
Normal file
22
lib/provider/spotify/views/home.dart
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
|
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
|
||||||
|
final homeViewProvider = FutureProvider((ref) async {
|
||||||
|
final country = ref.watch(
|
||||||
|
userPreferencesProvider.select((s) => s.recommendationMarket),
|
||||||
|
);
|
||||||
|
final spTCookie = ref.watch(
|
||||||
|
authenticationProvider.select((s) => s?.getCookie("sp_t")),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (spTCookie == null) return null;
|
||||||
|
|
||||||
|
final spotify = ref.watch(customSpotifyEndpointProvider);
|
||||||
|
|
||||||
|
return spotify.getHomeFeed(
|
||||||
|
country: country,
|
||||||
|
spTCookie: spTCookie,
|
||||||
|
);
|
||||||
|
});
|
26
lib/provider/spotify/views/home_section.dart
Normal file
26
lib/provider/spotify/views/home_section.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/models/spotify/home_feed.dart';
|
||||||
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
|
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
|
||||||
|
final homeSectionViewProvider =
|
||||||
|
FutureProvider.family<SpotifyHomeFeedSection?, String>(
|
||||||
|
(ref, sectionUri) async {
|
||||||
|
final country = ref.watch(
|
||||||
|
userPreferencesProvider.select((s) => s.recommendationMarket),
|
||||||
|
);
|
||||||
|
final spTCookie = ref.watch(
|
||||||
|
authenticationProvider.select((s) => s?.getCookie("sp_t")),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (spTCookie == null) return null;
|
||||||
|
|
||||||
|
final spotify = ref.watch(customSpotifyEndpointProvider);
|
||||||
|
|
||||||
|
return spotify.getHomeFeedSection(
|
||||||
|
sectionUri,
|
||||||
|
country: country,
|
||||||
|
spTCookie: spTCookie,
|
||||||
|
);
|
||||||
|
});
|
@ -2,7 +2,9 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/models/spotify/home_feed.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
|
import 'package:timezone/timezone.dart' as tz;
|
||||||
|
|
||||||
class CustomSpotifyEndpoints {
|
class CustomSpotifyEndpoints {
|
||||||
static const _baseUrl = 'https://api.spotify.com/v1';
|
static const _baseUrl = 'https://api.spotify.com/v1';
|
||||||
@ -175,4 +177,126 @@ class CustomSpotifyEndpoints {
|
|||||||
);
|
);
|
||||||
return SpotifyFriends.fromJson(jsonDecode(res.body));
|
return SpotifyFriends.fromJson(jsonDecode(res.body));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<SpotifyHomeFeed> getHomeFeed({
|
||||||
|
required String spTCookie,
|
||||||
|
required Market country,
|
||||||
|
}) async {
|
||||||
|
final headers = {
|
||||||
|
'app-platform': 'WebPlayer',
|
||||||
|
'authorization': 'Bearer $accessToken',
|
||||||
|
'content-type': 'application/json;charset=UTF-8',
|
||||||
|
'dnt': '1',
|
||||||
|
'origin': 'https://open.spotify.com',
|
||||||
|
'referer': 'https://open.spotify.com/'
|
||||||
|
};
|
||||||
|
final response = await http.get(
|
||||||
|
Uri(
|
||||||
|
scheme: "https",
|
||||||
|
host: "api-partner.spotify.com",
|
||||||
|
path: "/pathfinder/v1/query",
|
||||||
|
queryParameters: {
|
||||||
|
"operationName": "home",
|
||||||
|
"variables": jsonEncode({
|
||||||
|
"timeZone": tz.local.name,
|
||||||
|
"sp_t": spTCookie,
|
||||||
|
"country": country.name,
|
||||||
|
"facet": null,
|
||||||
|
"sectionItemsLimit": 10
|
||||||
|
}),
|
||||||
|
"extensions": jsonEncode(
|
||||||
|
{
|
||||||
|
"persistedQuery": {
|
||||||
|
"version": 1,
|
||||||
|
|
||||||
|
/// GraphQL persisted Query hash
|
||||||
|
/// This can change overtime. We've to lookout for it
|
||||||
|
/// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/
|
||||||
|
"sha256Hash":
|
||||||
|
"eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
headers: headers,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode >= 400) {
|
||||||
|
throw Exception(
|
||||||
|
"[RequestException] "
|
||||||
|
"Status: ${response.statusCode}\n"
|
||||||
|
"Body: ${response.body}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = SpotifyHomeFeed.fromJson(
|
||||||
|
transformHomeFeedJsonMap(
|
||||||
|
jsonDecode(response.body),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SpotifyHomeFeedSection> getHomeFeedSection(
|
||||||
|
String sectionUri, {
|
||||||
|
required String spTCookie,
|
||||||
|
required Market country,
|
||||||
|
}) async {
|
||||||
|
final headers = {
|
||||||
|
'app-platform': 'WebPlayer',
|
||||||
|
'authorization': 'Bearer $accessToken',
|
||||||
|
'content-type': 'application/json;charset=UTF-8',
|
||||||
|
'dnt': '1',
|
||||||
|
'origin': 'https://open.spotify.com',
|
||||||
|
'referer': 'https://open.spotify.com/'
|
||||||
|
};
|
||||||
|
final response = await http.get(
|
||||||
|
Uri(
|
||||||
|
scheme: "https",
|
||||||
|
host: "api-partner.spotify.com",
|
||||||
|
path: "/pathfinder/v1/query",
|
||||||
|
queryParameters: {
|
||||||
|
"operationName": "homeSection",
|
||||||
|
"variables": jsonEncode({
|
||||||
|
"timeZone": tz.local.name,
|
||||||
|
"sp_t": spTCookie,
|
||||||
|
"country": country.name,
|
||||||
|
"uri": sectionUri
|
||||||
|
}),
|
||||||
|
"extensions": jsonEncode(
|
||||||
|
{
|
||||||
|
"persistedQuery": {
|
||||||
|
"version": 1,
|
||||||
|
|
||||||
|
/// GraphQL persisted Query hash
|
||||||
|
/// This can change overtime. We've to lookout for it
|
||||||
|
/// Docs: https://www.apollographql.com/docs/graphos/operations/persisted-queries/
|
||||||
|
"sha256Hash":
|
||||||
|
"eb3fba2d388cf4fc4d696b1757a58584e9538a3b515ea742e9cc9465807340be",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
headers: headers,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode >= 400) {
|
||||||
|
throw Exception(
|
||||||
|
"[RequestException] "
|
||||||
|
"Status: ${response.statusCode}\n"
|
||||||
|
"Body: ${response.body}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = SpotifyHomeFeedSection.fromJson(
|
||||||
|
transformSectionItemJsonMap(
|
||||||
|
jsonDecode(response.body)["data"]["homeSections"]["sections"][0],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
10
pubspec.lock
10
pubspec.lock
@ -434,7 +434,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.3+5"
|
version: "0.3.3+5"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||||
@ -2238,6 +2238,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
|
timezone:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: timezone
|
||||||
|
sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.2"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -131,6 +131,8 @@ dependencies:
|
|||||||
lrc: ^1.0.2
|
lrc: ^1.0.2
|
||||||
pub_api_client: ^2.4.0
|
pub_api_client: ^2.4.0
|
||||||
pubspec_parse: ^1.2.2
|
pubspec_parse: ^1.2.2
|
||||||
|
timezone: ^0.9.2
|
||||||
|
crypto: ^3.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.9
|
build_runner: ^2.4.9
|
||||||
|
Loading…
Reference in New Issue
Block a user