2020-10-19 21:28:45 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2020-06-25 14:28:56 +02:00
|
|
|
import 'package:freezer/api/deezer.dart';
|
|
|
|
import 'package:freezer/api/download.dart';
|
|
|
|
import 'package:freezer/api/definitions.dart';
|
2020-10-19 21:28:45 +02:00
|
|
|
import 'package:freezer/settings.dart';
|
2020-06-25 14:28:56 +02:00
|
|
|
import 'package:html/parser.dart';
|
2020-10-19 21:28:45 +02:00
|
|
|
import 'package:html/dom.dart' as dom;
|
|
|
|
import 'package:http/http.dart' as http;
|
2020-06-25 14:28:56 +02:00
|
|
|
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
|
|
|
|
SpotifyAPI spotify = SpotifyAPI();
|
|
|
|
|
|
|
|
class SpotifyAPI {
|
|
|
|
|
|
|
|
SpotifyPlaylist importingSpotifyPlaylist;
|
|
|
|
StreamController importingStream = StreamController.broadcast();
|
|
|
|
bool doneImporting;
|
|
|
|
|
|
|
|
//Parse spotify URL to URI (spotify:track:1234)
|
|
|
|
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
|
|
|
|
String getEmbedUrl(String uri) => 'https://embed.spotify.com/?uri=$uri';
|
|
|
|
|
2020-12-04 18:02:50 +01:00
|
|
|
//https://link.tospotify.com/ or https://spotify.app.link/
|
|
|
|
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 {
|
|
|
|
if (url.contains("link.tospotify") || url.contains("spotify.app.link")) {
|
|
|
|
return parseUrl(await resolveLinkUrl(url));
|
|
|
|
}
|
|
|
|
return parseUrl(url);
|
|
|
|
}
|
|
|
|
|
2020-06-25 14:28:56 +02:00
|
|
|
//Extract JSON data form spotify embed page
|
|
|
|
Future<Map> getEmbedData(String url) async {
|
|
|
|
//Fetch
|
2020-10-19 21:28:45 +02:00
|
|
|
http.Response response = await http.get(url);
|
2020-06-25 14:28:56 +02:00
|
|
|
//Parse
|
2020-10-19 21:28:45 +02:00
|
|
|
dom.Document document = parse(response.body);
|
|
|
|
dom.Element element = document.getElementById('resource');
|
2020-11-09 22:05:47 +01:00
|
|
|
|
|
|
|
//Some are URL encoded
|
|
|
|
try {
|
|
|
|
return jsonDecode(element.innerHtml);
|
|
|
|
} catch (e) {
|
|
|
|
return jsonDecode(Uri.decodeComponent(element.innerHtml));
|
|
|
|
}
|
2020-06-25 14:28:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2020-10-09 20:52:45 +02:00
|
|
|
|
2020-11-09 22:05:47 +01:00
|
|
|
//Get Deezer track ID from Spotify URI
|
|
|
|
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);
|
|
|
|
return deezer['id'].toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
//Get Deezer album ID by UPC
|
|
|
|
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();
|
|
|
|
}
|
2020-06-25 14:28:56 +02:00
|
|
|
|
2020-10-19 21:28:45 +02:00
|
|
|
Future convertPlaylist(SpotifyPlaylist playlist, {bool downloadOnly = false, BuildContext context, AudioQuality quality}) async {
|
2020-06-25 14:28:56 +02:00
|
|
|
doneImporting = false;
|
|
|
|
importingSpotifyPlaylist = playlist;
|
|
|
|
|
|
|
|
//Create Deezer playlist
|
|
|
|
String playlistId;
|
|
|
|
if (!downloadOnly)
|
|
|
|
playlistId = await deezerAPI.createPlaylist(playlist.name, description: playlist.description);
|
|
|
|
|
|
|
|
//Search for tracks
|
2020-10-19 21:28:45 +02:00
|
|
|
List<Track> downloadTracks = [];
|
2020-06-25 14:28:56 +02:00
|
|
|
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)
|
2020-10-19 21:28:45 +02:00
|
|
|
downloadTracks.add(Track(id: id));
|
2020-06-25 14:28:56 +02:00
|
|
|
track.state = TrackImportState.OK;
|
|
|
|
} catch (e) {
|
|
|
|
//On error
|
|
|
|
track.state = TrackImportState.ERROR;
|
|
|
|
}
|
2020-10-19 21:28:45 +02:00
|
|
|
|
|
|
|
//Download
|
|
|
|
if (downloadOnly)
|
|
|
|
await downloadManager.addOfflinePlaylist(
|
|
|
|
Playlist(trackCount: downloadTracks.length, tracks: downloadTracks, title: playlist.name),
|
|
|
|
private: false,
|
|
|
|
quality: quality
|
|
|
|
);
|
|
|
|
|
2020-06-25 14:28:56 +02:00
|
|
|
//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;
|
|
|
|
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(', '),
|
|
|
|
isrc: json['external_ids']['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'],
|
2020-09-18 19:36:41 +02:00
|
|
|
image: (json['images'].length > 0) ? json['images'][0]['url'] : null,
|
2020-06-25 14:28:56 +02:00
|
|
|
tracks: json['tracks']['items'].map<SpotifyTrack>((j) => SpotifyTrack.fromJson(j['track'])).toList()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-11-09 22:05:47 +01:00
|
|
|
class SpotifyAlbum {
|
|
|
|
String upc;
|
|
|
|
|
|
|
|
SpotifyAlbum({this.upc});
|
|
|
|
|
|
|
|
//JSON
|
|
|
|
factory SpotifyAlbum.fromJson(Map json) => SpotifyAlbum(
|
|
|
|
upc: json['external_ids']['upc']
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-25 14:28:56 +02:00
|
|
|
enum TrackImportState {
|
|
|
|
NONE,
|
|
|
|
ERROR,
|
|
|
|
OK
|
|
|
|
}
|