2023-07-29 02:17:26 +00:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
import 'package:freezer/api/deezer.dart';
|
|
|
|
import 'package:freezer/api/importer.dart';
|
|
|
|
import 'package:freezer/settings.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:io';
|
|
|
|
|
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
|
|
|
|
class SpotifyScrapper {
|
|
|
|
//Parse spotify URL to URI (spotify:track:1234)
|
|
|
|
static String? parseUrl(String url) {
|
|
|
|
Uri uri = Uri.parse(url);
|
|
|
|
if (uri.pathSegments.length > 3) return null; //Invalid URL
|
2023-10-12 22:09:37 +00:00
|
|
|
if (uri.pathSegments.length == 3) {
|
2023-07-29 02:17:26 +00:00
|
|
|
return 'spotify:${uri.pathSegments[1]}:${uri.pathSegments[2]}';
|
2023-10-12 22:09:37 +00:00
|
|
|
}
|
|
|
|
if (uri.pathSegments.length == 2) {
|
2023-07-29 02:17:26 +00:00
|
|
|
return 'spotify:${uri.pathSegments[0]}:${uri.pathSegments[1]}';
|
2023-10-12 22:09:37 +00:00
|
|
|
}
|
2023-07-29 02:17:26 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
//Get spotify embed url from uri
|
|
|
|
static String getEmbedUrl(String uri) =>
|
|
|
|
'https://embed.spotify.com/?uri=$uri';
|
|
|
|
|
|
|
|
//https://link.tospotify.com/ or https://spotify.app.link/
|
|
|
|
static Future<String> 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)!;
|
|
|
|
}
|
|
|
|
|
|
|
|
static Future<String?> resolveUrl(String url) async {
|
|
|
|
if (url.contains("link.tospotify") || url.contains("spotify.app.link")) {
|
|
|
|
return parseUrl(await resolveLinkUrl(url));
|
|
|
|
}
|
|
|
|
return parseUrl(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
//Extract JSON data form spotify embed page
|
|
|
|
static Future<Map> getEmbedData(String url) async {
|
|
|
|
//Fetch
|
|
|
|
http.Response response = await http.get(Uri.parse(url));
|
|
|
|
//Parse
|
|
|
|
dom.Document document = parse(response.body);
|
|
|
|
dom.Element element = document.getElementById('resource')!;
|
|
|
|
|
|
|
|
//Some are URL encoded
|
|
|
|
try {
|
|
|
|
return jsonDecode(element.innerHtml);
|
|
|
|
} catch (e) {
|
|
|
|
return jsonDecode(Uri.decodeComponent(element.innerHtml));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static Future<SpotifyPlaylist> playlist(String uri) async {
|
|
|
|
//Load data
|
|
|
|
String url = getEmbedUrl(uri);
|
|
|
|
Map data = await getEmbedData(url);
|
|
|
|
//Parse
|
|
|
|
SpotifyPlaylist playlist = SpotifyPlaylist.fromJson(data);
|
|
|
|
return playlist;
|
|
|
|
}
|
|
|
|
|
|
|
|
//Get Deezer track ID from Spotify URI
|
|
|
|
static Future<String> convertTrack(String uri) async {
|
|
|
|
Map data = await getEmbedData(getEmbedUrl(uri));
|
|
|
|
SpotifyTrack track = SpotifyTrack.fromJson(data);
|
2023-10-12 22:09:37 +00:00
|
|
|
Map deezer = await deezerAPI.callPublicApi('track/isrc:${track.isrc!}');
|
2023-07-29 02:17:26 +00:00
|
|
|
return deezer['id'].toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
//Get Deezer album ID by UPC
|
|
|
|
static Future<String> convertAlbum(String uri) async {
|
|
|
|
Map data = await getEmbedData(getEmbedUrl(uri));
|
|
|
|
SpotifyAlbum album = SpotifyAlbum.fromJson(data);
|
2023-10-12 22:09:37 +00:00
|
|
|
Map deezer = await deezerAPI.callPublicApi('album/upc:${album.upc!}');
|
2023-07-29 02:17:26 +00:00
|
|
|
return deezer['id'].toString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class SpotifyTrack {
|
|
|
|
String? title;
|
|
|
|
List<String>? artists;
|
|
|
|
String? isrc;
|
|
|
|
|
|
|
|
SpotifyTrack({this.title, this.artists, this.isrc});
|
|
|
|
|
|
|
|
//JSON
|
|
|
|
factory SpotifyTrack.fromJson(Map json) => SpotifyTrack(
|
|
|
|
title: json['name'],
|
|
|
|
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 {
|
|
|
|
String? name;
|
|
|
|
String? description;
|
|
|
|
List<SpotifyTrack>? tracks;
|
|
|
|
String? image;
|
|
|
|
|
|
|
|
SpotifyPlaylist({this.name, this.description, this.tracks, this.image});
|
|
|
|
|
|
|
|
//JSON
|
|
|
|
factory SpotifyPlaylist.fromJson(Map json) => SpotifyPlaylist(
|
|
|
|
name: json['name'],
|
|
|
|
description: json['description'],
|
|
|
|
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 {
|
|
|
|
String? upc;
|
|
|
|
|
|
|
|
SpotifyAlbum({this.upc});
|
|
|
|
|
|
|
|
//JSON
|
|
|
|
factory SpotifyAlbum.fromJson(Map json) =>
|
|
|
|
SpotifyAlbum(upc: json['external_ids']['upc']);
|
|
|
|
}
|
|
|
|
|
|
|
|
class SpotifyAPIWrapper {
|
2023-10-20 23:12:33 +00:00
|
|
|
HttpServer? _server;
|
2023-07-29 02:17:26 +00:00
|
|
|
late SpotifyApi spotify;
|
|
|
|
late User me;
|
|
|
|
|
|
|
|
//Try authorize with saved credentials
|
|
|
|
Future<bool> trySaved() async {
|
|
|
|
if (settings.spotifyClientId == null ||
|
|
|
|
settings.spotifyClientSecret == null ||
|
|
|
|
settings.spotifyCredentials == null) return false;
|
|
|
|
final credentials = SpotifyApiCredentials(
|
|
|
|
settings.spotifyClientId, settings.spotifyClientSecret,
|
|
|
|
accessToken: settings.spotifyCredentials!.accessToken,
|
|
|
|
refreshToken: settings.spotifyCredentials!.refreshToken,
|
|
|
|
scopes: settings.spotifyCredentials!.scopes,
|
|
|
|
expiration: settings.spotifyCredentials!.expiration);
|
|
|
|
spotify = SpotifyApi(credentials);
|
|
|
|
me = await spotify.me.get();
|
|
|
|
await _save();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
late String responseUri;
|
|
|
|
//Get URL
|
|
|
|
final grant = SpotifyApi.authorizationCodeGrant(credentials);
|
2023-10-12 22:09:37 +00:00
|
|
|
const redirectUri = "http://localhost:42069";
|
2023-07-29 02:17:26 +00:00
|
|
|
final scopes = [
|
|
|
|
'user-read-private',
|
|
|
|
'playlist-read-private',
|
|
|
|
'playlist-read-collaborative',
|
|
|
|
'user-library-read'
|
|
|
|
];
|
|
|
|
final authUri =
|
|
|
|
grant.getAuthorizationUrl(Uri.parse(redirectUri), scopes: scopes);
|
|
|
|
launchUrl(authUri);
|
|
|
|
//Wait for code
|
2023-10-20 23:12:33 +00:00
|
|
|
|
|
|
|
await for (HttpRequest request in _server!) {
|
2023-07-29 02:17:26 +00:00
|
|
|
//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) {
|
2023-10-20 23:12:33 +00:00
|
|
|
_server!.close();
|
2023-07-29 02:17:26 +00:00
|
|
|
responseUri = request.uri.toString();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
//Create spotify
|
|
|
|
spotify = SpotifyApi.fromAuthCodeGrant(grant, responseUri);
|
|
|
|
me = await spotify.me.get();
|
|
|
|
|
|
|
|
//Save
|
|
|
|
await _save();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future _save() async {
|
|
|
|
//Save credentials
|
|
|
|
final spotifyCredentials = await spotify.getCredentials();
|
|
|
|
final saveCredentials = SpotifyCredentialsSave(
|
|
|
|
accessToken: spotifyCredentials.accessToken,
|
|
|
|
refreshToken: spotifyCredentials.refreshToken,
|
|
|
|
scopes: spotifyCredentials.scopes,
|
|
|
|
expiration: spotifyCredentials.expiration);
|
|
|
|
|
|
|
|
settings.spotifyClientSecret = spotifyCredentials.clientId;
|
|
|
|
settings.spotifyClientSecret = spotifyCredentials.clientSecret;
|
|
|
|
settings.spotifyCredentials = saveCredentials;
|
|
|
|
await settings.save();
|
|
|
|
}
|
|
|
|
|
|
|
|
//Cancel authorization
|
|
|
|
void cancelAuthorize() {
|
2023-10-20 23:12:33 +00:00
|
|
|
_server?.close(force: true);
|
2023-07-29 02:17:26 +00:00
|
|
|
}
|
|
|
|
}
|