2025-08-02 04:48:34 +02:00
|
|
|
// Spotify API Credentials
|
|
|
|
//
|
|
|
|
// SETUP INSTRUCTIONS:
|
|
|
|
// 1. Go to https://developer.spotify.com/dashboard
|
|
|
|
// 2. Log in with your Spotify account
|
|
|
|
// 3. Create a new app called "SleepySound"
|
|
|
|
// 4. Copy your Client ID and Client Secret below
|
|
|
|
// 5. Save this file
|
|
|
|
//
|
|
|
|
// SECURITY NOTE: Never commit real credentials to version control!
|
|
|
|
// For production, use environment variables or secure storage.
|
|
|
|
|
|
|
|
|
2025-08-02 04:16:15 +02:00
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../models/spotify_track.dart';
|
2025-08-02 04:48:34 +02:00
|
|
|
import 'SPOTIFY_SECRET.dart';
|
2025-08-02 04:16:15 +02:00
|
|
|
|
|
|
|
class SpotifyService {
|
2025-08-02 04:48:34 +02:00
|
|
|
// Load credentials from the secret file
|
|
|
|
static String get _clientId => SpotifyCredentials.clientId;
|
|
|
|
static String get _clientSecret => SpotifyCredentials.clientSecret;
|
2025-08-02 04:16:15 +02:00
|
|
|
static const String _baseUrl = 'https://api.spotify.com/v1';
|
|
|
|
static const String _authUrl = 'https://accounts.spotify.com/api/token';
|
|
|
|
|
|
|
|
String? _accessToken;
|
|
|
|
|
2025-08-02 04:48:34 +02:00
|
|
|
// Check if valid credentials are provided
|
|
|
|
bool get _hasValidCredentials =>
|
|
|
|
_clientId != 'YOUR_SPOTIFY_CLIENT_ID' &&
|
|
|
|
_clientSecret != 'YOUR_SPOTIFY_CLIENT_SECRET' &&
|
|
|
|
_clientId.isNotEmpty &&
|
|
|
|
_clientSecret.isNotEmpty;
|
|
|
|
|
2025-08-02 04:16:15 +02:00
|
|
|
// For demo purposes, we'll use Client Credentials flow (no user login required)
|
|
|
|
// In a real app, you'd want to implement Authorization Code flow for user-specific features
|
|
|
|
|
|
|
|
Future<void> _getAccessToken() async {
|
2025-08-02 04:48:34 +02:00
|
|
|
// Check if we have valid credentials first
|
|
|
|
if (!_hasValidCredentials) {
|
|
|
|
print('No valid Spotify credentials found. Using demo data.');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-08-02 04:16:15 +02:00
|
|
|
try {
|
|
|
|
final response = await http.post(
|
|
|
|
Uri.parse(_authUrl),
|
|
|
|
headers: {
|
|
|
|
'Authorization': 'Basic ${base64Encode(utf8.encode('$_clientId:$_clientSecret'))}',
|
|
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
|
|
},
|
|
|
|
body: 'grant_type=client_credentials',
|
|
|
|
);
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
final data = json.decode(response.body);
|
|
|
|
_accessToken = data['access_token'];
|
|
|
|
|
|
|
|
// Save token to shared preferences
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
await prefs.setString('spotify_access_token', _accessToken!);
|
|
|
|
|
|
|
|
print('Spotify access token obtained successfully');
|
|
|
|
} else {
|
|
|
|
print('Failed to get Spotify access token: ${response.statusCode}');
|
|
|
|
print('Response body: ${response.body}');
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
print('Error getting Spotify access token: $e');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _ensureValidToken() async {
|
2025-08-02 04:48:34 +02:00
|
|
|
// If no valid credentials, skip token generation
|
|
|
|
if (!_hasValidCredentials) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-08-02 04:16:15 +02:00
|
|
|
if (_accessToken == null) {
|
|
|
|
// Try to load from shared preferences first
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
_accessToken = prefs.getString('spotify_access_token');
|
|
|
|
|
|
|
|
if (_accessToken == null) {
|
|
|
|
await _getAccessToken();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<SpotifyTrack>> searchTracks(String query, {int limit = 20}) async {
|
|
|
|
try {
|
|
|
|
await _ensureValidToken();
|
|
|
|
|
2025-08-02 04:48:34 +02:00
|
|
|
// If no valid credentials or token, use demo data
|
|
|
|
if (!_hasValidCredentials || _accessToken == null) {
|
|
|
|
print('Using demo data for search: $query');
|
2025-08-02 04:16:15 +02:00
|
|
|
return _getDemoTracks(query);
|
|
|
|
}
|
|
|
|
|
|
|
|
final encodedQuery = Uri.encodeQueryComponent(query);
|
|
|
|
final response = await http.get(
|
|
|
|
Uri.parse('$_baseUrl/search?q=$encodedQuery&type=track&limit=$limit'),
|
|
|
|
headers: {
|
|
|
|
'Authorization': 'Bearer $_accessToken',
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
final data = json.decode(response.body);
|
|
|
|
final searchResponse = SpotifySearchResponse.fromJson(data);
|
2025-08-02 04:48:34 +02:00
|
|
|
print('Found ${searchResponse.tracks.items.length} tracks from Spotify API');
|
2025-08-02 04:16:15 +02:00
|
|
|
return searchResponse.tracks.items;
|
|
|
|
} else if (response.statusCode == 401) {
|
|
|
|
// Token expired, get a new one
|
|
|
|
_accessToken = null;
|
|
|
|
await _getAccessToken();
|
|
|
|
return searchTracks(query, limit: limit); // Retry
|
|
|
|
} else {
|
|
|
|
print('Spotify search failed: ${response.statusCode}');
|
|
|
|
print('Response body: ${response.body}');
|
|
|
|
return _getDemoTracks(query);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
print('Error searching Spotify: $e');
|
|
|
|
return _getDemoTracks(query);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<SpotifyTrack>> getPopularTracks({String genre = 'chill'}) async {
|
|
|
|
try {
|
|
|
|
await _ensureValidToken();
|
|
|
|
|
2025-08-02 04:48:34 +02:00
|
|
|
if (!_hasValidCredentials || _accessToken == null) {
|
|
|
|
print('Using demo popular tracks');
|
2025-08-02 04:16:15 +02:00
|
|
|
return _getDemoPopularTracks();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Search for popular tracks in the genre
|
|
|
|
final response = await http.get(
|
|
|
|
Uri.parse('$_baseUrl/search?q=genre:$genre&type=track&limit=10'),
|
|
|
|
headers: {
|
|
|
|
'Authorization': 'Bearer $_accessToken',
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
final data = json.decode(response.body);
|
|
|
|
final searchResponse = SpotifySearchResponse.fromJson(data);
|
|
|
|
return searchResponse.tracks.items;
|
|
|
|
} else {
|
|
|
|
return _getDemoPopularTracks();
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
print('Error getting popular tracks: $e');
|
|
|
|
return _getDemoPopularTracks();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Demo data for when Spotify API is not available
|
|
|
|
List<SpotifyTrack> _getDemoTracks(String query) {
|
|
|
|
final demoTracks = [
|
|
|
|
_createDemoTrack('1', 'Tropical House Cruises', 'Kygo', 'Cloud Nine', 'https://i.scdn.co/image/tropical'),
|
|
|
|
_createDemoTrack('2', 'Summer Breeze', 'Seeb', 'Summer Hits', 'https://i.scdn.co/image/summer'),
|
|
|
|
_createDemoTrack('3', 'Relaxing Waves', 'Chillhop Music', 'Chill Collection', 'https://i.scdn.co/image/waves'),
|
|
|
|
_createDemoTrack('4', 'Sunset Vibes', 'Odesza', 'In Return', 'https://i.scdn.co/image/sunset'),
|
|
|
|
_createDemoTrack('5', 'Ocean Dreams', 'Emancipator', 'Soon It Will Be Cold Enough', 'https://i.scdn.co/image/ocean'),
|
|
|
|
];
|
|
|
|
|
|
|
|
// Filter based on query
|
|
|
|
if (query.toLowerCase().contains('tropical') || query.toLowerCase().contains('kygo')) {
|
|
|
|
return [demoTracks[0]];
|
|
|
|
} else if (query.toLowerCase().contains('summer')) {
|
|
|
|
return [demoTracks[1]];
|
|
|
|
} else if (query.toLowerCase().contains('chill') || query.toLowerCase().contains('relax')) {
|
|
|
|
return [demoTracks[2], demoTracks[4]];
|
|
|
|
} else if (query.toLowerCase().contains('sunset')) {
|
|
|
|
return [demoTracks[3]];
|
|
|
|
}
|
|
|
|
|
|
|
|
return demoTracks;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<SpotifyTrack> _getDemoPopularTracks() {
|
|
|
|
return [
|
|
|
|
_createDemoTrack('pop1', 'Ocean Breeze', 'Lofi Dreams', 'Summer Collection', 'https://i.scdn.co/image/ocean'),
|
|
|
|
_createDemoTrack('pop2', 'Sunset Melody', 'Acoustic Soul', 'Peaceful Moments', 'https://i.scdn.co/image/sunset'),
|
|
|
|
_createDemoTrack('pop3', 'Peaceful Waters', 'Nature Sounds', 'Tranquil Vibes', 'https://i.scdn.co/image/water'),
|
|
|
|
_createDemoTrack('pop4', 'Summer Nights', 'Chill Vibes', 'Evening Sessions', 'https://i.scdn.co/image/night'),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
SpotifyTrack _createDemoTrack(String id, String name, String artistName, String albumName, String imageUrl) {
|
|
|
|
return SpotifyTrack(
|
|
|
|
id: id,
|
|
|
|
name: name,
|
|
|
|
artists: [SpotifyArtist(id: 'artist_$id', name: artistName)],
|
|
|
|
album: SpotifyAlbum(
|
|
|
|
id: 'album_$id',
|
|
|
|
name: albumName,
|
|
|
|
images: [SpotifyImage(height: 640, width: 640, url: imageUrl)],
|
|
|
|
),
|
|
|
|
durationMs: 210000 + (id.hashCode % 120000), // Random duration between 3:30 and 5:30
|
|
|
|
externalUrls: {'spotify': 'https://open.spotify.com/track/$id'},
|
|
|
|
previewUrl: null,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-08-02 04:48:34 +02:00
|
|
|
// Method to check if Spotify API is properly configured
|
|
|
|
static bool get isConfigured =>
|
|
|
|
SpotifyCredentials.clientId != 'YOUR_SPOTIFY_CLIENT_ID' &&
|
|
|
|
SpotifyCredentials.clientSecret != 'YOUR_SPOTIFY_CLIENT_SECRET' &&
|
|
|
|
SpotifyCredentials.clientId.isNotEmpty &&
|
|
|
|
SpotifyCredentials.clientSecret.isNotEmpty;
|
|
|
|
|
|
|
|
// Method to get configuration status for UI display
|
|
|
|
static String get configurationStatus {
|
|
|
|
if (isConfigured) {
|
|
|
|
return 'Spotify API configured ✓';
|
|
|
|
} else {
|
|
|
|
return 'Using demo data (Spotify not configured)';
|
|
|
|
}
|
2025-08-02 04:16:15 +02:00
|
|
|
}
|
|
|
|
}
|