spotify_preview_finder 1.0.0
spotify_preview_finder: ^1.0.0 copied to clipboard
A Flutter package to search Spotify tracks, scrape 30-second preview URLs (bypassing the deprecated preview_url API restriction), and play them.
import 'package:flutter/material.dart';
import 'package:spotify_preview_finder/spotify_preview_finder.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Spotify Preview Finder Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark().copyWith(
primaryColor: const Color(0xFF1DB954),
scaffoldBackgroundColor: const Color(0xFF121212),
colorScheme: const ColorScheme.dark(
primary: Color(0xFF1DB954),
surface: Color(0xFF181818),
),
),
home: const DashboardScreen(),
);
}
}
class DashboardScreen extends StatefulWidget {
const DashboardScreen({Key? key}) : super(key: key);
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
final _clientIdController = TextEditingController();
final _clientSecretController = TextEditingController();
final _songController = TextEditingController(text: 'Blinding Lights');
final _artistController = TextEditingController(text: 'The Weeknd');
bool _isLoading = false;
String? _accessToken;
List<SpotifyTrack> _tracks = [];
String? _errorMessage;
// Sharing a single audio player across the results list ensuring only
// one track plays at a time.
final SpotifyPreviewPlayer _sharedPlayer = SpotifyPreviewPlayer();
@override
void dispose() {
_clientIdController.dispose();
_clientSecretController.dispose();
_songController.dispose();
_artistController.dispose();
_sharedPlayer.dispose();
super.dispose();
}
Future<void> _fetchTokenAndSearch() async {
final clientId = _clientIdController.text.trim();
final clientSecret = _clientSecretController.text.trim();
final songName = _songController.text.trim();
final artistName = _artistController.text.trim();
if (clientId.isEmpty || clientSecret.isEmpty) {
setState(() {
_errorMessage =
'Please enter both Spotify Client ID and Client Secret.';
});
return;
}
if (songName.isEmpty) {
setState(() {
_errorMessage = 'Please enter a song name to search.';
});
return;
}
setState(() {
_isLoading = true;
_errorMessage = null;
_tracks = [];
});
try {
// 1. Fetch access token from Spotify Developer Accounts API
final token = await SpotifyPreviewFinder.getAccessToken(
clientId: clientId,
clientSecret: clientSecret,
);
_accessToken = token;
// 2. Search Spotify Web API for matching tracks and scrape preview MP3s in parallel
final results = await SpotifyPreviewFinder.searchAndGetPreviews(
songName: songName,
artist: artistName.isNotEmpty ? artistName : null,
accessToken: token,
limit: 5,
);
setState(() {
_tracks = results;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'Failed to load tracks: $e';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Row(
children: [
Icon(Icons.library_music, color: Color(0xFF1DB954), size: 28),
SizedBox(width: 10),
Text(
'Spotify Preview Finder',
style: TextStyle(fontWeight: FontWeight.w900, letterSpacing: 0.5),
),
],
),
backgroundColor: const Color(0xFF000000),
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Info Header Banner
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF1DB954).withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFF1DB954).withOpacity(0.3),
),
),
child: const Row(
children: [
Icon(Icons.info_outline, color: Color(0xFF1DB954)),
SizedBox(width: 10),
Expanded(
child: Text(
'Bypasses deprecated Spotify Web API preview_url limitations by scraping raw MP3 hashes from public web interfaces.',
style: TextStyle(fontSize: 12, color: Colors.white70),
),
),
],
),
),
const SizedBox(height: 20),
// Credentials Dashboard Card
Card(
color: const Color(0xFF181818),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Spotify Credentials',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 14),
TextField(
controller: _clientIdController,
decoration: const InputDecoration(
labelText: 'Client ID',
hintText: 'Enter your Spotify Client ID',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.vpn_key_outlined),
isDense: true,
),
),
const SizedBox(height: 14),
TextField(
controller: _clientSecretController,
decoration: const InputDecoration(
labelText: 'Client Secret',
hintText: 'Enter your Spotify Client Secret',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock_outline),
isDense: true,
),
obscureText: true,
),
const SizedBox(height: 8),
const Text(
'Credentials can be fetched from developer.spotify.com/dashboard',
style: TextStyle(fontSize: 11, color: Colors.white30),
),
],
),
),
),
const SizedBox(height: 16),
// Search Parameter Controls Card
Card(
color: const Color(0xFF181818),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Search Spotify Tracks',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 14),
TextField(
controller: _songController,
decoration: const InputDecoration(
labelText: 'Song Title',
hintText: 'e.g., Blinding Lights',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.audiotrack_outlined),
isDense: true,
),
),
const SizedBox(height: 14),
TextField(
controller: _artistController,
decoration: const InputDecoration(
labelText: 'Artist (Optional)',
hintText: 'e.g., The Weeknd',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person_outline),
isDense: true,
),
),
const SizedBox(height: 18),
ElevatedButton(
onPressed: _isLoading ? null : _fetchTokenAndSearch,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1DB954),
foregroundColor: Colors.black,
shadowColor: const Color(0xFF1DB954).withOpacity(0.3),
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: _isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.black,
),
),
)
: const Text(
'Search & Scrape Previews',
style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 15,
),
),
),
],
),
),
),
const SizedBox(height: 18),
// Error Display Banner
if (_errorMessage != null) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red[900]?.withOpacity(0.2),
border: Border.all(color: Colors.red.withOpacity(0.4)),
borderRadius: BorderRadius.circular(10),
),
child: Text(
_errorMessage!,
style: const TextStyle(color: Colors.redAccent, fontSize: 13),
),
),
const SizedBox(height: 18),
],
// Tracks Results List
if (_tracks.isNotEmpty) ...[
const Padding(
padding: EdgeInsets.only(left: 4, bottom: 12),
child: Text(
'Discovered Track Previews',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 0.2,
),
),
),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _tracks.length,
separatorBuilder: (context, index) =>
const SizedBox(height: 14),
itemBuilder: (context, index) {
final track = _tracks[index];
return SpotifyPreviewPlayerWidget(
track: track,
player: _sharedPlayer,
);
},
),
] else if (!_isLoading &&
_errorMessage == null &&
_accessToken != null)
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 40.0),
child: Text(
'No preview tracks found.',
style: TextStyle(color: Colors.white30, fontSize: 14),
),
),
),
],
),
),
);
}
}