spotify_preview_finder 1.0.0 copy "spotify_preview_finder: ^1.0.0" to clipboard
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.

example/lib/main.dart

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),
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }
}
1
likes
140
points
76
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter package to search Spotify tracks, scrape 30-second preview URLs (bypassing the deprecated preview_url API restriction), and play them.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

audioplayers, flutter, http

More

Packages that depend on spotify_preview_finder