0.6.10 - Spotify is ass

This commit is contained in:
exttex 2021-04-05 22:22:32 +02:00
parent 3105ed6c1d
commit 676d5d45cc
23 changed files with 1000 additions and 339 deletions

View file

@ -4,7 +4,6 @@ import 'package:freezer/api/spotify.dart';
import 'package:freezer/settings.dart';
import 'package:http/http.dart' as http;
import 'dart:io';
import 'dart:convert';
import 'dart:async';
@ -149,12 +148,12 @@ class DeezerAPI {
try {
//Tracks
if (uri.pathSegments[0] == 'track') {
String id = await spotify.convertTrack(spotifyUri);
String id = await SpotifyScrapper.convertTrack(spotifyUri);
return DeezerLinkResponse(type: DeezerLinkType.TRACK, id: id);
}
//Albums
if (uri.pathSegments[0] == 'album') {
String id = await spotify.convertAlbum(spotifyUri);
String id = await SpotifyScrapper.convertAlbum(spotifyUri);
return DeezerLinkResponse(type: DeezerLinkType.ALBUM, id: id);
}
} catch (e) {}

171
lib/api/importer.dart Normal file
View file

@ -0,0 +1,171 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/download.dart';
Importer importer = Importer();
class Importer {
//Options
bool download = false;
//Preserve context
BuildContext context;
String title;
String description;
List<ImporterTrack> tracks;
String playlistId;
Playlist playlist;
bool done = false;
bool busy = false;
Future _future;
StreamController _streamController;
Stream get updateStream => _streamController.stream;
int get ok => tracks.fold(0, (v, t) => (t.state == TrackImportState.OK) ? v+1 : v);
int get error => tracks.fold(0, (v, t) => (t.state == TrackImportState.ERROR) ? v+1 : v);
Importer();
//Start importing wrapper
Future<void> start(BuildContext context, String title, String description, List<ImporterTrack> tracks) async {
//Save variables
this.playlist = null;
this.context = context;
this.title = title;
this.description = description??'';
this.tracks = tracks.map((t) {t.state = TrackImportState.NONE; return t;}).toList();
//Create playlist
playlistId = await deezerAPI.createPlaylist(title, description: description);
busy = true;
done = false;
_streamController = StreamController.broadcast();
_future = _start();
}
//Start importer
Future _start() async {
for (int i=0; i<tracks.length; i++) {
try {
String id = await _searchTrack(tracks[i]);
//Not found
if (id == null) {
tracks[i].state = TrackImportState.ERROR;
_streamController.add(tracks[i]);
continue;
}
//Add to playlist
await deezerAPI.addToPlaylist(id, playlistId);
tracks[i].state = TrackImportState.OK;
} catch (_) {
//Error occurred, mark as error
tracks[i].state = TrackImportState.ERROR;
}
_streamController.add(tracks[i]);
}
//Get full playlist
playlist = await deezerAPI.playlist(playlistId, nb: 10000);
playlist.library = true;
//Download
if (download) {
await downloadManager.addOfflinePlaylist(playlist, private: false, context: context);
}
//Mark as done
done = true;
busy = false;
//To update UI
_streamController.add(null);
_streamController.close();
}
//Find track on Deezer servers
Future<String> _searchTrack(ImporterTrack track) async {
//Try by ISRC
if (track.isrc != null && track.isrc.length == 12) {
Map deezer = await deezerAPI.callPublicApi('track/isrc:' + track.isrc);
if (deezer["id"] != null) {
return deezer["id"].toString();
}
}
//Search
String cleanedTitle = track.title.trim().toLowerCase().replaceAll("-", "").replaceAll("&", "").replaceAll("+", "");
SearchResults results = await deezerAPI.search("${track.artists[0]} $cleanedTitle");
for (Track t in results.tracks) {
//Match title
if (_cleanMatching(t.title) == _cleanMatching(track.title)) {
//Match artist
if (_matchArtists(track.artists, t.artists.map((a) => a.name))) {
return t.id;
}
}
}
}
//Clean title for matching
String _cleanMatching(String t) {
return t.toLowerCase()
.replaceAll(",", "")
.replaceAll("-", "")
.replaceAll(" ", "")
.replaceAll("&", "")
.replaceAll("+", "")
.replaceAll("/", "");
}
String _cleanArtist(String a) {
return a.toLowerCase()
.replaceAll(" ", "")
.replaceAll(",", "");
}
//Match at least 1 artist
bool _matchArtists(List<String> a, List<String> b) {
//Clean
List<String> _a = a.map(_cleanArtist).toList();
List<String> _b = b.map(_cleanArtist).toList();
for (String artist in _a) {
if (_b.contains(artist)) {
return true;
}
}
return false;
}
}
class ImporterTrack {
String title;
List<String> artists;
String isrc;
TrackImportState state;
ImporterTrack(this.title, this.artists, {this.isrc, this.state = TrackImportState.NONE});
}
enum TrackImportState {
NONE,
ERROR,
OK
}
extension TrackImportStateExtension on TrackImportState {
Widget get icon {
switch (this) {
case TrackImportState.ERROR:
return Icon(Icons.error, color: Colors.red,);
case TrackImportState.OK:
return Icon(Icons.done, color: Colors.green);
default:
return Container(width: 0, height: 0);
}
}
}

View file

@ -1,26 +1,18 @@
import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/api/importer.dart';
import 'package:html/parser.dart';
import 'package:html/dom.dart' as dom;
import 'package:http/http.dart' as http;
import 'package:spotify/spotify.dart';
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'package:url_launcher/url_launcher.dart';
SpotifyAPI spotify = SpotifyAPI();
class SpotifyAPI {
SpotifyPlaylist importingSpotifyPlaylist;
StreamController importingStream = StreamController.broadcast();
bool doneImporting;
class SpotifyScrapper {
//Parse spotify URL to URI (spotify:track:1234)
String parseUrl(String url) {
static String parseUrl(String url) {
Uri uri = Uri.parse(url);
if (uri.pathSegments.length > 3) return null; //Invalid URL
if (uri.pathSegments.length == 3) return 'spotify:${uri.pathSegments[1]}:${uri.pathSegments[2]}';
@ -29,16 +21,16 @@ class SpotifyAPI {
}
//Get spotify embed url from uri
String getEmbedUrl(String uri) => 'https://embed.spotify.com/?uri=$uri';
static String getEmbedUrl(String uri) => 'https://embed.spotify.com/?uri=$uri';
//https://link.tospotify.com/ or https://spotify.app.link/
Future resolveLinkUrl(String url) async {
static Future resolveLinkUrl(String url) async {
http.Response response = await http.get(Uri.parse(url));
Match match = RegExp(r'window\.top\.location = validate\("(.+)"\);').firstMatch(response.body);
return match.group(1);
}
Future resolveUrl(String url) async {
static Future resolveUrl(String url) async {
if (url.contains("link.tospotify") || url.contains("spotify.app.link")) {
return parseUrl(await resolveLinkUrl(url));
}
@ -46,7 +38,7 @@ class SpotifyAPI {
}
//Extract JSON data form spotify embed page
Future<Map> getEmbedData(String url) async {
static Future<Map> getEmbedData(String url) async {
//Fetch
http.Response response = await http.get(url);
//Parse
@ -61,7 +53,7 @@ class SpotifyAPI {
}
}
Future<SpotifyPlaylist> playlist(String uri) async {
static Future<SpotifyPlaylist> playlist(String uri) async {
//Load data
String url = getEmbedUrl(uri);
Map data = await getEmbedData(url);
@ -71,7 +63,7 @@ class SpotifyAPI {
}
//Get Deezer track ID from Spotify URI
Future<String> convertTrack(String uri) async {
static Future<String> convertTrack(String uri) async {
Map data = await getEmbedData(getEmbedUrl(uri));
SpotifyTrack track = SpotifyTrack.fromJson(data);
Map deezer = await deezerAPI.callPublicApi('track/isrc:' + track.isrc);
@ -79,75 +71,34 @@ class SpotifyAPI {
}
//Get Deezer album ID by UPC
Future<String> convertAlbum(String uri) async {
static Future<String> convertAlbum(String uri) async {
Map data = await getEmbedData(getEmbedUrl(uri));
SpotifyAlbum album = SpotifyAlbum.fromJson(data);
Map deezer = await deezerAPI.callPublicApi('album/upc:' + album.upc);
return deezer['id'].toString();
}
Future convertPlaylist(SpotifyPlaylist playlist, {bool downloadOnly = false, BuildContext context, AudioQuality quality}) async {
doneImporting = false;
importingSpotifyPlaylist = playlist;
//Create Deezer playlist
String playlistId;
if (!downloadOnly)
playlistId = await deezerAPI.createPlaylist(playlist.name, description: playlist.description);
//Search for tracks
List<Track> downloadTracks = [];
for (SpotifyTrack track in playlist.tracks) {
Map deezer;
try {
//Search
deezer = await deezerAPI.callPublicApi('track/isrc:' + track.isrc);
if (deezer.containsKey('error')) throw Exception();
String id = deezer['id'].toString();
//Add
if (!downloadOnly)
await deezerAPI.addToPlaylist(id, playlistId);
if (downloadOnly)
downloadTracks.add(Track(id: id));
track.state = TrackImportState.OK;
} catch (e) {
//On error
track.state = TrackImportState.ERROR;
}
//Download
if (downloadOnly)
await downloadManager.addOfflinePlaylist(
Playlist(trackCount: downloadTracks.length, tracks: downloadTracks, title: playlist.name),
private: false,
quality: quality
);
//Add playlist id to stream, stream is for updating ui only
importingStream.add(playlistId);
importingSpotifyPlaylist = playlist;
}
doneImporting = true;
//Return DEEZER playlist id
return playlistId;
}
}
class SpotifyTrack {
String title;
String artists;
List<String> artists;
String isrc;
TrackImportState state = TrackImportState.NONE;
SpotifyTrack({this.title, this.artists, this.isrc});
//JSON
factory SpotifyTrack.fromJson(Map json) => SpotifyTrack(
title: json['name'],
artists: json['artists'].map((j) => j['name']).toList().join(', '),
artists: json['artists'].map<String>((a) => a["name"].toString()).toList(),
isrc: json['external_ids']['isrc']
);
//Convert track to importer track
ImporterTrack toImporter() {
return ImporterTrack(title, artists, isrc: isrc);
}
}
class SpotifyPlaylist {
@ -165,6 +116,11 @@ class SpotifyPlaylist {
image: (json['images'].length > 0) ? json['images'][0]['url'] : null,
tracks: json['tracks']['items'].map<SpotifyTrack>((j) => SpotifyTrack.fromJson(j['track'])).toList()
);
//Convert to importer tracks
List<ImporterTrack> toImporter() {
return tracks.map((t) => t.toImporter()).toList();
}
}
class SpotifyAlbum {
@ -178,8 +134,50 @@ class SpotifyAlbum {
);
}
enum TrackImportState {
NONE,
ERROR,
OK
class SpotifyAPIWrapper {
HttpServer _server;
SpotifyApi spotify;
User me;
Future authorize(String clientId, String clientSecret) async {
//Spotify
SpotifyApiCredentials credentials = SpotifyApiCredentials(clientId, clientSecret);
spotify = SpotifyApi(credentials);
//Create server
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, 42069);
String responseUri;
//Get URL
final grant = SpotifyApi.authorizationCodeGrant(credentials);
final redirectUri = "http://localhost:42069";
final scopes = ['user-read-private', 'playlist-read-private', 'playlist-read-collaborative', 'user-library-read'];
final authUri = grant.getAuthorizationUrl(Uri.parse(redirectUri), scopes: scopes);
launch(authUri.toString());
//Wait for code
await for (HttpRequest request in _server) {
//Exit window
request.response.headers.set("Content-Type", "text/html; charset=UTF-8");
request.response.write("<body><h1>You can close this page and go back to Freezer.</h1></body><script>window.close();</script>");
request.response.close();
//Get token
if (request.uri.queryParameters["code"] != null) {
_server.close();
_server = null;
responseUri = request.uri.toString();
break;
}
}
//Create spotify
spotify = SpotifyApi.fromAuthCodeGrant(grant, responseUri);
me = await spotify.me.get();
}
//Cancel authorization
void cancelAuthorize() {
if (_server != null) {
_server.close(force: true);
_server = null;
}
}
}