spotify integration works
This commit is contained in:
parent
f70fe3cdd1
commit
025eee7644
11 changed files with 1637 additions and 717 deletions
204
CHALLENGE_2/sleepysound/lib/services/music_queue_service.dart
Normal file
204
CHALLENGE_2/sleepysound/lib/services/music_queue_service.dart
Normal file
|
@ -0,0 +1,204 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import '../models/spotify_track.dart';
|
||||
import '../services/spotify_service.dart';
|
||||
|
||||
class QueueItem {
|
||||
final SpotifyTrack track;
|
||||
int votes;
|
||||
bool userVoted;
|
||||
final DateTime addedAt;
|
||||
|
||||
QueueItem({
|
||||
required this.track,
|
||||
this.votes = 1,
|
||||
this.userVoted = true,
|
||||
DateTime? addedAt,
|
||||
}) : addedAt = addedAt ?? DateTime.now();
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': track.id,
|
||||
'title': track.name,
|
||||
'artist': track.artistNames,
|
||||
'votes': votes,
|
||||
'userVoted': userVoted,
|
||||
'duration': track.duration,
|
||||
'imageUrl': track.imageUrl,
|
||||
'addedAt': addedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
class MusicQueueService extends ChangeNotifier {
|
||||
static final MusicQueueService _instance = MusicQueueService._internal();
|
||||
factory MusicQueueService() => _instance;
|
||||
MusicQueueService._internal();
|
||||
|
||||
final SpotifyService _spotifyService = SpotifyService();
|
||||
|
||||
// Current playing track
|
||||
SpotifyTrack? _currentTrack;
|
||||
bool _isPlaying = false;
|
||||
double _progress = 0.0;
|
||||
|
||||
// Queue management
|
||||
final List<QueueItem> _queue = [];
|
||||
|
||||
// Recently played
|
||||
final List<SpotifyTrack> _recentlyPlayed = [];
|
||||
|
||||
// Getters
|
||||
SpotifyTrack? get currentTrack => _currentTrack;
|
||||
bool get isPlaying => _isPlaying;
|
||||
double get progress => _progress;
|
||||
List<QueueItem> get queue => List.unmodifiable(_queue);
|
||||
List<SpotifyTrack> get recentlyPlayed => List.unmodifiable(_recentlyPlayed);
|
||||
|
||||
// Queue operations
|
||||
void addToQueue(SpotifyTrack track) {
|
||||
// Check if track is already in queue
|
||||
final existingIndex = _queue.indexWhere((item) => item.track.id == track.id);
|
||||
|
||||
if (existingIndex != -1) {
|
||||
// If track exists, upvote it
|
||||
upvote(existingIndex);
|
||||
} else {
|
||||
// Add new track to queue
|
||||
final queueItem = QueueItem(track: track);
|
||||
_queue.add(queueItem);
|
||||
_sortQueue();
|
||||
notifyListeners();
|
||||
print('Added "${track.name}" by ${track.artistNames} to queue');
|
||||
}
|
||||
}
|
||||
|
||||
void upvote(int index) {
|
||||
if (index >= 0 && index < _queue.length) {
|
||||
_queue[index].votes++;
|
||||
if (!_queue[index].userVoted) {
|
||||
_queue[index].userVoted = true;
|
||||
}
|
||||
_sortQueue();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void downvote(int index) {
|
||||
if (index >= 0 && index < _queue.length) {
|
||||
if (_queue[index].votes > 0) {
|
||||
_queue[index].votes--;
|
||||
}
|
||||
_sortQueue();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _sortQueue() {
|
||||
_queue.sort((a, b) {
|
||||
// First sort by votes (descending)
|
||||
final voteComparison = b.votes.compareTo(a.votes);
|
||||
if (voteComparison != 0) return voteComparison;
|
||||
|
||||
// If votes are equal, sort by time added (ascending - first come first serve)
|
||||
return a.addedAt.compareTo(b.addedAt);
|
||||
});
|
||||
}
|
||||
|
||||
// Playback simulation
|
||||
void playNext() {
|
||||
if (_queue.isNotEmpty) {
|
||||
final nextItem = _queue.removeAt(0);
|
||||
_currentTrack = nextItem.track;
|
||||
_isPlaying = true;
|
||||
_progress = 0.0;
|
||||
|
||||
// Add to recently played
|
||||
_recentlyPlayed.insert(0, nextItem.track);
|
||||
if (_recentlyPlayed.length > 10) {
|
||||
_recentlyPlayed.removeLast();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
print('Now playing: ${_currentTrack!.name} by ${_currentTrack!.artistNames}');
|
||||
|
||||
// Simulate track progress
|
||||
_simulatePlayback();
|
||||
}
|
||||
}
|
||||
|
||||
void togglePlayPause() {
|
||||
_isPlaying = !_isPlaying;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void skipTrack() {
|
||||
playNext();
|
||||
}
|
||||
|
||||
void _simulatePlayback() {
|
||||
if (_currentTrack == null) return;
|
||||
|
||||
// Simulate track progress over time
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (_isPlaying && _currentTrack != null) {
|
||||
_progress += 1.0 / (_currentTrack!.durationMs / 1000);
|
||||
if (_progress >= 1.0) {
|
||||
// Track finished, play next
|
||||
playNext();
|
||||
} else {
|
||||
notifyListeners();
|
||||
_simulatePlayback();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize with some popular tracks
|
||||
Future<void> initializeQueue() async {
|
||||
if (_queue.isEmpty && _currentTrack == null) {
|
||||
try {
|
||||
final popularTracks = await _spotifyService.getPopularTracks();
|
||||
for (final track in popularTracks.take(4)) {
|
||||
final queueItem = QueueItem(
|
||||
track: track,
|
||||
votes: 10 - popularTracks.indexOf(track) * 2, // Decreasing votes
|
||||
userVoted: false,
|
||||
);
|
||||
_queue.add(queueItem);
|
||||
}
|
||||
|
||||
// Start playing the first track
|
||||
if (_queue.isNotEmpty) {
|
||||
playNext();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
print('Error initializing queue: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
Future<List<SpotifyTrack>> searchTracks(String query) async {
|
||||
return await _spotifyService.searchTracks(query, limit: 20);
|
||||
}
|
||||
|
||||
// Get queue as JSON for display
|
||||
List<Map<String, dynamic>> get queueAsJson {
|
||||
return _queue.map((item) => item.toJson()).toList();
|
||||
}
|
||||
|
||||
// Get current track info for display
|
||||
Map<String, dynamic>? get currentTrackInfo {
|
||||
if (_currentTrack == null) return null;
|
||||
|
||||
return {
|
||||
'title': _currentTrack!.name,
|
||||
'artist': _currentTrack!.artistNames,
|
||||
'album': _currentTrack!.album.name,
|
||||
'imageUrl': _currentTrack!.imageUrl,
|
||||
'duration': _currentTrack!.duration,
|
||||
'progress': _progress,
|
||||
'isPlaying': _isPlaying,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,20 +1,48 @@
|
|||
// 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.
|
||||
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/spotify_track.dart';
|
||||
import 'SPOTIFY_SECRET.dart';
|
||||
|
||||
class SpotifyService {
|
||||
static const String _clientId = 'YOUR_SPOTIFY_CLIENT_ID'; // You'll need to get this from Spotify Developer Console
|
||||
static const String _clientSecret = 'YOUR_SPOTIFY_CLIENT_SECRET'; // You'll need to get this from Spotify Developer Console
|
||||
// Load credentials from the secret file
|
||||
static String get _clientId => SpotifyCredentials.clientId;
|
||||
static String get _clientSecret => SpotifyCredentials.clientSecret;
|
||||
static const String _baseUrl = 'https://api.spotify.com/v1';
|
||||
static const String _authUrl = 'https://accounts.spotify.com/api/token';
|
||||
|
||||
String? _accessToken;
|
||||
|
||||
// Check if valid credentials are provided
|
||||
bool get _hasValidCredentials =>
|
||||
_clientId != 'YOUR_SPOTIFY_CLIENT_ID' &&
|
||||
_clientSecret != 'YOUR_SPOTIFY_CLIENT_SECRET' &&
|
||||
_clientId.isNotEmpty &&
|
||||
_clientSecret.isNotEmpty;
|
||||
|
||||
// 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 {
|
||||
// Check if we have valid credentials first
|
||||
if (!_hasValidCredentials) {
|
||||
print('No valid Spotify credentials found. Using demo data.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse(_authUrl),
|
||||
|
@ -44,6 +72,11 @@ class SpotifyService {
|
|||
}
|
||||
|
||||
Future<void> _ensureValidToken() async {
|
||||
// If no valid credentials, skip token generation
|
||||
if (!_hasValidCredentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_accessToken == null) {
|
||||
// Try to load from shared preferences first
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
@ -59,8 +92,9 @@ class SpotifyService {
|
|||
try {
|
||||
await _ensureValidToken();
|
||||
|
||||
if (_accessToken == null) {
|
||||
// Return demo data if no token available
|
||||
// If no valid credentials or token, use demo data
|
||||
if (!_hasValidCredentials || _accessToken == null) {
|
||||
print('Using demo data for search: $query');
|
||||
return _getDemoTracks(query);
|
||||
}
|
||||
|
||||
|
@ -75,6 +109,7 @@ class SpotifyService {
|
|||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
final searchResponse = SpotifySearchResponse.fromJson(data);
|
||||
print('Found ${searchResponse.tracks.items.length} tracks from Spotify API');
|
||||
return searchResponse.tracks.items;
|
||||
} else if (response.statusCode == 401) {
|
||||
// Token expired, get a new one
|
||||
|
@ -96,7 +131,8 @@ class SpotifyService {
|
|||
try {
|
||||
await _ensureValidToken();
|
||||
|
||||
if (_accessToken == null) {
|
||||
if (!_hasValidCredentials || _accessToken == null) {
|
||||
print('Using demo popular tracks');
|
||||
return _getDemoPopularTracks();
|
||||
}
|
||||
|
||||
|
@ -170,12 +206,19 @@ class SpotifyService {
|
|||
);
|
||||
}
|
||||
|
||||
// Method to initialize with your Spotify credentials
|
||||
static void setCredentials(String clientId, String clientSecret) {
|
||||
// In a real app, you'd store these securely
|
||||
// For demo purposes, we'll use the demo data
|
||||
print('Spotify credentials would be set here in a real app');
|
||||
print('Client ID: $clientId');
|
||||
print('For demo purposes, using mock data instead');
|
||||
// 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)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue