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:
# avoid_print: false # Uncomment to disable the `avoid_print` 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
# https://dart.dev/guides/language/analysis-options

View File

@ -7,7 +7,12 @@ import 'package:spotube/provider/SpotifyDI.dart';
class CategoryCard extends StatefulWidget {
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
_CategoryCardState createState() => _CategoryCardState();
@ -35,6 +40,7 @@ class _CategoryCardState extends State<CategoryCard> {
return PlaylistGenreView(
widget.category.id!,
widget.category.name!,
playlists: widget.playlists,
);
},
),
@ -46,25 +52,34 @@ class _CategoryCardState extends State<CategoryCard> {
),
),
Consumer<SpotifyDI>(
builder: (context, data, child) =>
FutureBuilder<Page<PlaylistSimple>>(
future: data.spotifyApi.playlists
.getByCategoryId(widget.category.id!)
.getPage(4, 0),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text("Error occurred"));
}
if (!snapshot.hasData) {
return const Center(child: Text("Loading.."));
}
return Wrap(
spacing: 20,
children: snapshot.data!.items!
.map((playlist) => PlaylistCard(playlist))
.toList(),
builder: (context, data, child) {
return FutureBuilder<Iterable<PlaylistSimple>>(
future: widget.playlists == null
? (widget.category.id != "user-featured-playlists"
? data.spotifyApi.playlists
.getByCategoryId(widget.category.id!)
: data.spotifyApi.playlists.featured)
.getPage(4, 0)
.then((value) => value.items ?? [])
: Future.value(widget.playlists),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text("Error occurred"));
}
if (!snapshot.hasData) {
return const Center(
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")
.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) {
_pagingController.appendLastPage(categories.items!.toList());
_pagingController.appendLastPage(items);
} else if (categories.items != null) {
_pagingController.appendPage(
categories.items!.toList(), categories.nextOffset);
_pagingController.appendPage(items, categories.nextOffset);
}
} catch (e) {
_pagingController.error = e;
@ -119,7 +126,6 @@ class _HomeState extends State<Home> {
child: Row(
children: [
NavigationRail(
backgroundColor: Colors.blueGrey[50],
destinations: sidebarTileList
.map((e) => NavigationRailDestination(
icon: Icon(e.icon),

View File

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:mpv_dart/mpv_dart.dart';
import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class Player extends StatefulWidget {
const Player({Key? key}) : super(key: key);
@ -220,9 +221,32 @@ class _PlayerState extends State<Player> {
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.favorite_outline_rounded),
onPressed: () {}),
Consumer<SpotifyDI>(builder: (context, data, widget) {
return FutureBuilder<bool>(
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(
constraints: const BoxConstraints(maxWidth: 200),
child: Slider.adaptive(

View File

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

View File

@ -20,7 +20,9 @@ class _UserLibraryState extends State<UserLibrary> {
future: data.spotifyApi.playlists.me.all(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator.adaptive();
return const Expanded(
child: Center(child: CircularProgressIndicator.adaptive()),
);
}
Image image = Image();
image.height = 300;
@ -40,8 +42,8 @@ class _UserLibraryState extends State<UserLibrary> {
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Wrap(
spacing: 8.0, // gap between adjacent chips
runSpacing: 8.0, // gap between lines
spacing: 20, // gap between adjacent chips
runSpacing: 20, // gap between lines
alignment: WrapAlignment.center,
children: [
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(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
title: 'Spotube',
theme: ThemeData(
primaryColor: Colors.greenAccent[400],
primarySwatch: Colors.green,
@ -87,6 +87,9 @@ class MyApp extends StatelessWidget {
color: Colors.grey[800]!,
),
),
),
navigationRailTheme: NavigationRailThemeData(
backgroundColor: Colors.blueGrey[50],
)),
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,
});
}