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 if (uri.pathSegments.length == 3) { return 'spotify:${uri.pathSegments[1]}:${uri.pathSegments[2]}'; } if (uri.pathSegments.length == 2) { return 'spotify:${uri.pathSegments[0]}:${uri.pathSegments[1]}'; } 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 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 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 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 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 convertTrack(String uri) async { Map data = await getEmbedData(getEmbedUrl(uri)); SpotifyTrack track = SpotifyTrack.fromJson(data); Map deezer = await deezerAPI.callPublicApi('track/isrc:${track.isrc!}'); return deezer['id'].toString(); } //Get Deezer album ID by UPC static Future 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(); } } class SpotifyTrack { String? title; List? artists; String? isrc; SpotifyTrack({this.title, this.artists, this.isrc}); //JSON factory SpotifyTrack.fromJson(Map json) => SpotifyTrack( title: json['name'], artists: json['artists'].map((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? 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((j) => SpotifyTrack.fromJson(j['track'])) .toList()); //Convert to importer tracks List 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 { late HttpServer _server; late SpotifyApi spotify; late User me; //Try authorize with saved credentials Future 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); const 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); launchUrl(authUri); //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( "

You can close this page and go back to Freezer.

"); request.response.close(); //Get token if (request.uri.queryParameters["code"] != null) { _server.close(); 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() { _server.close(force: true); } }