Save Track button support (incomplete)

Featured Playlist support
This commit is contained in:
Kingkor Roy Tirtho 2022-01-05 13:41:34 +06:00
parent 96629f6a83
commit f3ecb24d3b
13 changed files with 95 additions and 37250 deletions

View File

@ -24,6 +24,7 @@ linter:
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
file_names: false
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@ -7,7 +7,12 @@ import 'package:spotube/provider/SpotifyDI.dart';
class CategoryCard extends StatefulWidget { class CategoryCard extends StatefulWidget {
final Category category; final Category category;
const CategoryCard(this.category, {Key? key}) : super(key: key); final Iterable<PlaylistSimple>? playlists;
const CategoryCard(
this.category, {
Key? key,
this.playlists,
}) : super(key: key);
@override @override
_CategoryCardState createState() => _CategoryCardState(); _CategoryCardState createState() => _CategoryCardState();
@ -35,6 +40,7 @@ class _CategoryCardState extends State<CategoryCard> {
return PlaylistGenreView( return PlaylistGenreView(
widget.category.id!, widget.category.id!,
widget.category.name!, widget.category.name!,
playlists: widget.playlists,
); );
}, },
), ),
@ -46,25 +52,34 @@ class _CategoryCardState extends State<CategoryCard> {
), ),
), ),
Consumer<SpotifyDI>( Consumer<SpotifyDI>(
builder: (context, data, child) => builder: (context, data, child) {
FutureBuilder<Page<PlaylistSimple>>( return FutureBuilder<Iterable<PlaylistSimple>>(
future: data.spotifyApi.playlists future: widget.playlists == null
.getByCategoryId(widget.category.id!) ? (widget.category.id != "user-featured-playlists"
.getPage(4, 0), ? data.spotifyApi.playlists
builder: (context, snapshot) { .getByCategoryId(widget.category.id!)
if (snapshot.hasError) { : data.spotifyApi.playlists.featured)
return const Center(child: Text("Error occurred")); .getPage(4, 0)
} .then((value) => value.items ?? [])
if (!snapshot.hasData) { : Future.value(widget.playlists),
return const Center(child: Text("Loading..")); builder: (context, snapshot) {
} if (snapshot.hasError) {
return Wrap( return const Center(child: Text("Error occurred"));
spacing: 20, }
children: snapshot.data!.items! if (!snapshot.hasData) {
.map((playlist) => PlaylistCard(playlist)) return const Center(
.toList(), child: CircularProgressIndicator.adaptive(),
); );
}), }
return Wrap(
spacing: 20,
runSpacing: 20,
children: snapshot.data!
.map((playlist) => PlaylistCard(playlist))
.toList(),
);
});
},
) )
], ],
); );

View File

@ -83,11 +83,18 @@ class _HomeState extends State<Home> {
.list(country: "US") .list(country: "US")
.getPage(15, pageKey); .getPage(15, pageKey);
var items = categories.items!.toList();
if (pageKey == 0) {
Category category = Category();
category.id = "user-featured-playlists";
category.name = "Featured";
items.insert(0, category);
}
if (categories.isLast && categories.items != null) { if (categories.isLast && categories.items != null) {
_pagingController.appendLastPage(categories.items!.toList()); _pagingController.appendLastPage(items);
} else if (categories.items != null) { } else if (categories.items != null) {
_pagingController.appendPage( _pagingController.appendPage(items, categories.nextOffset);
categories.items!.toList(), categories.nextOffset);
} }
} catch (e) { } catch (e) {
_pagingController.error = e; _pagingController.error = e;
@ -119,7 +126,6 @@ class _HomeState extends State<Home> {
child: Row( child: Row(
children: [ children: [
NavigationRail( NavigationRail(
backgroundColor: Colors.blueGrey[50],
destinations: sidebarTileList destinations: sidebarTileList
.map((e) => NavigationRailDestination( .map((e) => NavigationRailDestination(
icon: Icon(e.icon), icon: Icon(e.icon),

View File

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:mpv_dart/mpv_dart.dart'; import 'package:mpv_dart/mpv_dart.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class Player extends StatefulWidget { class Player extends StatefulWidget {
const Player({Key? key}) : super(key: key); const Player({Key? key}) : super(key: key);
@ -220,9 +221,32 @@ class _PlayerState extends State<Player> {
child: Wrap( child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
IconButton( Consumer<SpotifyDI>(builder: (context, data, widget) {
icon: const Icon(Icons.favorite_outline_rounded), return FutureBuilder<bool>(
onPressed: () {}), future: playback.currentTrack?.id != null
? data.spotifyApi.tracks.me
.containsOne(playback.currentTrack!.id!)
: Future.value(false),
initialData: false,
builder: (context, snapshot) {
bool isLiked = snapshot.data ?? false;
return IconButton(
icon: Icon(
!isLiked
? Icons.favorite_outline_rounded
: Icons.favorite_rounded,
color: isLiked ? Colors.green : null,
),
onPressed: () {
if (!isLiked &&
playback.currentTrack?.id != null) {
data.spotifyApi.tracks.me
.saveOne(playback.currentTrack!.id!)
.then((value) => setState(() {}));
}
});
});
}),
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200), constraints: const BoxConstraints(maxWidth: 200),
child: Slider.adaptive( child: Slider.adaptive(

View File

@ -7,8 +7,13 @@ import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistGenreView extends StatefulWidget { class PlaylistGenreView extends StatefulWidget {
final String genreId; final String genreId;
final String genreName; final String genreName;
const PlaylistGenreView(this.genreId, this.genreName, {Key? key}) final Iterable<PlaylistSimple>? playlists;
: super(key: key); const PlaylistGenreView(
this.genreId,
this.genreName, {
this.playlists,
Key? key,
}) : super(key: key);
@override @override
_PlaylistGenreViewState createState() => _PlaylistGenreViewState(); _PlaylistGenreViewState createState() => _PlaylistGenreViewState();
} }
@ -37,9 +42,13 @@ class _PlaylistGenreViewState extends State<PlaylistGenreView> {
builder: (context, data, child) => Expanded( builder: (context, data, child) => Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
child: FutureBuilder<Iterable<PlaylistSimple>>( child: FutureBuilder<Iterable<PlaylistSimple>>(
future: data.spotifyApi.playlists future: widget.playlists == null
.getByCategoryId(widget.genreId) ? (widget.genreId != "user-featured-playlists"
.all(), ? data.spotifyApi.playlists
.getByCategoryId(widget.genreId)
.all()
: data.spotifyApi.playlists.featured.all())
: Future.value(widget.playlists),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return const Center(child: Text("Error occurred")); return const Center(child: Text("Error occurred"));

View File

@ -20,7 +20,9 @@ class _UserLibraryState extends State<UserLibrary> {
future: data.spotifyApi.playlists.me.all(), future: data.spotifyApi.playlists.me.all(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const CircularProgressIndicator.adaptive(); return const Expanded(
child: Center(child: CircularProgressIndicator.adaptive()),
);
} }
Image image = Image(); Image image = Image();
image.height = 300; image.height = 300;
@ -40,8 +42,8 @@ class _UserLibraryState extends State<UserLibrary> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Wrap( child: Wrap(
spacing: 8.0, // gap between adjacent chips spacing: 20, // gap between adjacent chips
runSpacing: 8.0, // gap between lines runSpacing: 20, // gap between lines
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
children: [ children: [
PlaylistCard(likedTracksPlaylist), PlaylistCard(likedTracksPlaylist),

View File

@ -1,145 +0,0 @@
import 'dart:convert';
import 'package:spotube/models/YoutubeTrack.dart';
import 'package:http/http.dart';
import 'package:spotube/models/YoutubeSearchResult.dart';
import 'package:spotify/spotify.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
Future<List<YtSearchResult>> searchYoutube(String query,
{int limit = 20}) async {
try {
if (query.trim().isEmpty) throw Exception("query can't be blank");
Client client = Client();
Uri url = Uri(
scheme: "https",
host: "www.youtube.com",
path: "results",
queryParameters: {
"search_query": query,
},
);
Response page = await client.get(url);
return parseSearch(page.body, limit);
} catch (e) {
throw e;
}
}
List<YtSearchResult> parseSearch(String html, int limit) {
List<YtSearchResult> results = [];
List<dynamic> dataInfo = [];
bool scrapped = false;
try {
var initDoc = html.split("var ytInitialData = ");
String data = initDoc[1].split(";</script><script")[0];
html = data;
} catch (e) {
print("[Error extracting ytInitialData]: $e");
}
try {
var decodedHtml = jsonDecode(html)["contents"]
["twoColumnSearchResultsRenderer"]["primaryContents"]
["sectionListRenderer"]["contents"]
.first["itemSectionRenderer"]["contents"];
dataInfo = decodedHtml;
scrapped = true;
} catch (e) {
print("[Error accessing itemSectionRenderer.contents]: $e");
}
if (!scrapped) {
try {
dataInfo = jsonDecode(html
.split("{\"itemSectionRenderer\":")
.last
.split("},{\"continuationItemRenderer\":{")
.first)["contents"];
scrapped = true;
} catch (err) {
print(
"[Error in try again <accessing itemSectionRenderer.contents>]: $err");
}
}
// failure
if (!scrapped) {
return [];
}
for (var data in dataInfo) {
try {
YtSearchResult result;
data = data["videoRenderer"];
if (data == null) continue;
result = YtSearchResult(
id: data["videoId"],
title: data["title"]["runs"].first["text"],
duration: "unavailable",
thumbnail: data["thumbnail"]["thumbnails"].last["url"],
channel: YtChannel(
id: data["ownerText"]["runs"].first["navigationEndpoint"]
["browseEndpoint"]["browseId"],
name: data["ownerText"]["runs"].first["text"],
url: "https://www.youtube.com" +
data["ownerText"]["runs"].first["navigationEndpoint"]
["browseEndpoint"]["canonicalBaseUrl"],
),
uploadDate: "unavailable",
viewCount: "unavailable",
type: "video");
results.add(result);
} catch (e) {
print("[Error in construction of result]: $e");
}
}
return results;
}
Future<Track> findYtVariant(Track track) async {
YoutubeExplode youtube = YoutubeExplode();
double includePercentage(String src, List matches) {
int count = 0;
matches.forEach((match) => {
if (src.contains(match.toString())) {count++}
});
return (count / matches.length) * 100;
}
var artistsName = track.artists?.map((ar) => ar.name).toList() ?? [];
String queryString =
"${artistsName.first} - ${track.name}${artistsName.length > 1 ? " feat. ${artistsName.sublist(1).join(" ")}" : ""}";
SearchList videos = await youtube.search.getVideos(queryString);
List<YoutubeRelevantTrack> tracksWithRelevance =
await Future.wait(videos.map((video) async {
double matchPercentage = includePercentage(video.title, [
track.name,
...artistsName,
]);
Channel channel = await youtube.channels.get(video.channelId);
bool sameChannel = (artistsName.first != null
? channel.title.contains(artistsName.first!)
: false) ||
(artistsName.first?.contains(channel.title) ?? false);
return YoutubeRelevantTrack(
url: video.url,
matchPercentage: matchPercentage,
sameChannel: sameChannel,
id: video.id.value);
}));
tracksWithRelevance.sort((a, b) {
return a.matchPercentage.compareTo(b.matchPercentage);
});
List<YoutubeRelevantTrack> sameChannelTracks =
tracksWithRelevance.where((tr) => tr.sameChannel).toList();
track.uri = (sameChannelTracks.isNotEmpty
? sameChannelTracks.first.url
: tracksWithRelevance.isNotEmpty
? tracksWithRelevance.first.url
: videos.first.url);
return track;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -55,7 +55,7 @@ class MyApp extends StatelessWidget {
], ],
child: MaterialApp( child: MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'Flutter Demo', title: 'Spotube',
theme: ThemeData( theme: ThemeData(
primaryColor: Colors.greenAccent[400], primaryColor: Colors.greenAccent[400],
primarySwatch: Colors.green, primarySwatch: Colors.green,
@ -87,6 +87,9 @@ class MyApp extends StatelessWidget {
color: Colors.grey[800]!, color: Colors.grey[800]!,
), ),
), ),
),
navigationRailTheme: NavigationRailThemeData(
backgroundColor: Colors.blueGrey[50],
)), )),
home: const Home(), home: const Home(),
), ),

View File

@ -1,72 +0,0 @@
import 'dart:convert';
class YtSearchResult {
String? id;
String? title;
String? duration;
String? thumbnail;
YtChannel? channel;
String? uploadDate;
String? viewCount;
String? type;
YtSearchResult(
{this.id,
this.title,
this.duration,
this.thumbnail,
this.channel,
this.uploadDate,
this.viewCount,
this.type});
YtSearchResult.fromJson(Map<String, dynamic> json) {
id = json['id'];
title = json['title'];
duration = json['duration'];
thumbnail = json['thumbnail'];
channel = json['channel'] != null
? new YtChannel.fromJson(json['channel'])
: null;
uploadDate = json['uploadDate'];
viewCount = json['viewCount'];
type = json['type'];
}
String toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['title'] = this.title;
data['duration'] = this.duration;
data['thumbnail'] = this.thumbnail;
if (this.channel != null) {
data['channel'] = this.channel?.toJson();
}
data['uploadDate'] = this.uploadDate;
data['viewCount'] = this.viewCount;
data['type'] = this.type;
return jsonEncode(data);
}
}
class YtChannel {
String? id;
String? name;
String? url;
YtChannel({this.id, this.name, this.url});
YtChannel.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
url = json['url'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['name'] = this.name;
data['url'] = this.url;
return data;
}
}

View File

@ -1,14 +0,0 @@
import 'package:spotify/spotify.dart';
class YoutubeRelevantTrack {
String url;
double matchPercentage;
bool sameChannel;
String id;
YoutubeRelevantTrack({
required this.url,
required this.matchPercentage,
required this.sameChannel,
required this.id,
});
}