yt_flutter_musicapi 3.3.1 copy "yt_flutter_musicapi: ^3.3.1" to clipboard
yt_flutter_musicapi: ^3.3.1 copied to clipboard

A Flutter plugin for YouTube Music API that provides music search and related songs functionality using Python backend.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:yt_flutter_musicapi/yt_flutter_musicapi.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'YouTube Music API Test',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        brightness: Brightness.dark,
        primarySwatch: Colors.blue,
      ),
      home: MusicApiTestPage(),
    );
  }
}

class AppSettings {
  static int limit = 5;
  static AudioQuality audioQuality = AudioQuality.veryHigh;
  static ThumbnailQuality thumbnailQuality = ThumbnailQuality.veryHigh;
  static bool isDarkMode = false;
  static String mode = 'auto'; // 'auto', 'batch', or 'stream'
  static String artistName = 'Ed Sheeran';
  static List<Map<String, String>> testSongs = [
    {'title': 'Perfect', 'artist': 'Ed Sheeran'},
    {'title': 'Bad Guy', 'artist': 'Billie Eilish'},
    {'title': 'Blinding Lights', 'artist': 'The Weeknd'},
  ];
  static String relatedSongTitle = 'Perfect';
  static String relatedSongArtist = 'Ed Sheeran';
}

class Inspector {
  static void checkRules(List<dynamic> results, String operation) {
    print('๐Ÿ” INSPECTOR: Checking rules for $operation');

    if (results.isEmpty) {
      print('โŒ INSPECTOR: No results returned');
      return;
    }

    if (results.length > AppSettings.limit) {
      print(
        'โŒ INSPECTOR: Limit exceeded! Expected: ${AppSettings.limit}, Got: ${results.length}',
      );
    } else {
      print(
        'โœ… INSPECTOR: Limit respected: ${results.length}/${AppSettings.limit}',
      );
    }

    for (int i = 0; i < results.length; i++) {
      var item = results[i];
      print('๐Ÿ“‹ INSPECTOR: Item ${i + 1}:');

      if (item is SearchResult) {
        _checkSearchResult(item);
      } else if (item is RelatedSong) {
        _checkRelatedSong(item);
      } else if (item is ArtistSong) {
        _checkArtistSong(item);
      } else if (item is SongDetail) {
        _checkSongDetail(item);
      }
    }
  }

  static void _checkArtistSong(ArtistSong song) {
    print('  Title: ${song.title}');
    print('  Artists: ${song.artists}');
    print('  Video ID: ${song.videoId}');
    print('  Duration: ${song.duration ?? 'N/A'}');
    print('  Artist Name: ${song.artistName}');

    if (song.albumArt != null) {
      print(
        '  โœ… Album Art: Available (${AppSettings.thumbnailQuality.value} quality)',
      );
    } else {
      print('  โŒ Album Art: Missing');
    }

    if (song.audioUrl != null) {
      print(
        '  โœ… Audio URL: Available (${AppSettings.audioQuality.value} quality)',
      );
    } else {
      print('  โŒ Audio URL: Missing');
    }
    print('  ---');
  }

  static void _checkSongDetail(SongDetail song) {
    print('  Title: ${song.title}');
    print('  Artists: ${song.artists}');
    print('  Video ID: ${song.videoId}');
    print('  Duration: ${song.duration ?? 'N/A'}');
    print('  Year: ${song.year ?? 'N/A'}');
    print('  Album: ${song.album ?? 'N/A'}');

    if (song.albumArt != null) {
      print(
        '  โœ… Album Art: Available (${AppSettings.thumbnailQuality.value} quality)',
      );
    } else {
      print('  โŒ Album Art: Missing');
    }

    if (song.audioUrl != null) {
      print(
        '  โœ… Audio URL: Available (${AppSettings.audioQuality.value} quality)',
      );
    } else {
      print('  โŒ Audio URL: Missing');
    }
    print('  ---');
  }

  static void _checkSearchResult(SearchResult result) {
    print('  Title: ${result.title}');
    print('  Artists: ${result.artists}');
    print('  Video ID: ${result.videoId}');
    print('  Duration: ${result.duration ?? 'N/A'}');
    print('  Year: ${result.year ?? 'N/A'}');

    if (result.albumArt != null) {
      print(
        '  โœ… Album Art: Available (${AppSettings.thumbnailQuality.value} quality)',
      );
    } else {
      print('  โŒ Album Art: Missing');
    }

    if (result.audioUrl != null) {
      print(
        '  โœ… Audio URL: Available (${AppSettings.audioQuality.value} quality)',
      );
    } else {
      print('  โŒ Audio URL: Missing');
    }
    print('  ---');
  }

  static void _checkRelatedSong(RelatedSong song) {
    print('  Title: ${song.title}');
    print('  Artists: ${song.artists}');
    print('  Video ID: ${song.videoId}');
    print('  Duration: ${song.duration ?? 'N/A'}');
    print('  Is Original: ${song.isOriginal}');

    if (song.albumArt != null) {
      print(
        '  โœ… Album Art: Available (${AppSettings.thumbnailQuality.value} quality)',
      );
    } else {
      print('  โŒ Album Art: Missing');
    }

    if (song.audioUrl != null) {
      print(
        '  โœ… Audio URL: Available (${AppSettings.audioQuality.value} quality)',
      );
    } else {
      print('  โŒ Audio URL: Missing');
    }
    print('  ---');
  }

  static void checkSingleSongDetail(SongDetail? songDetail, String operation) {
    print('๐Ÿ” INSPECTOR: Checking single song detail for $operation');

    if (songDetail == null) {
      print('โŒ INSPECTOR: No song detail returned');
      return;
    }

    print('๐Ÿ“‹ INSPECTOR: Song Detail:');
    _checkSongDetail(songDetail);
  }
}

class MusicApiTestPage extends StatefulWidget {
  @override
  _MusicApiTestPageState createState() => _MusicApiTestPageState();
}

class _MusicApiTestPageState extends State<MusicApiTestPage> {
  final YtFlutterMusicapi _api = YtFlutterMusicapi();
  final List<String> _cliOutput = [];
  final ScrollController _scrollController = ScrollController();
  final TextEditingController _searchController = TextEditingController();
  bool _isInitialized = false;
  bool _isLoading = false;
  final List<ArtistSong> _songs = [];

  @override
  void initState() {
    super.initState();
    _addToCliOutput('๐Ÿš€ YouTube Music API Test App Started');
    _addToCliOutput('โ„น๏ธ  Use the buttons below to test API methods');
    _addToCliOutput('โš™๏ธ  Configure settings using the gear icon');
  }

  void _addToCliOutput(String message) {
    setState(() {
      _cliOutput.add(
        '${DateTime.now().toString().substring(11, 19)} | $message',
      );
    });

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent,
          duration: Duration(milliseconds: 300),
          curve: Curves.easeOut,
        );
      }
    });
  }

  void _clearCliOutput() {
    setState(() {
      _cliOutput.clear();
    });
    _addToCliOutput('๐Ÿงน CLI Output Cleared');
  }

  Future<void> _initializeApi() async {
    if (_isLoading) return;

    setState(() {
      _isLoading = true;
    });

    _addToCliOutput('๐Ÿ”„ Initializing YouTube Music API...');

    try {
      final response = await _api.initialize(country: 'US');

      if (response.success) {
        setState(() {
          _isInitialized = true;
        });
        _addToCliOutput('โœ… API Initialized Successfully');
        _addToCliOutput('๐Ÿ“‹ Message: ${response.message ?? 'Ready to use'}');
      } else {
        _addToCliOutput('โŒ API Initialization Failed');
        _addToCliOutput('๐Ÿ“‹ Error: ${response.error ?? 'Unknown error'}');
      }
    } catch (e) {
      _addToCliOutput('โŒ Exception during initialization: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _checkStatus() async {
    if (_isLoading) return;

    setState(() {
      _isLoading = true;
    });

    _addToCliOutput('๐Ÿ” Checking API Status...');

    try {
      final response = await _api.checkStatus();

      if (response.success && response.data != null) {
        final status = response.data!;

        _addToCliOutput('โœ… API Status: OK');
        _addToCliOutput('๐Ÿ“‹ Message: ${status.message}');

        _addToCliOutput(
          '๐ŸŽต YTMusic: ${status.ytmusicReady ? 'โœ… Ready' : 'โŒ Not Ready'} (v${status.ytmusicVersion})',
        );

        _addToCliOutput(
          'โฌ‡๏ธ yt-dlp: ${status.ytdlpReady ? 'โœ… Ready' : 'โŒ Not Ready'} (v${status.ytdlpVersion})',
        );

        if (status.isFullyOperational) {
          _addToCliOutput('๐Ÿš€ All systems operational and ready!');
          _addToCliOutput('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”');
          _addToCliOutput('๐Ÿ“Š System Summary:');
          _addToCliOutput('   โ€ข YTMusic API: โœ… Operational');
          _addToCliOutput('   โ€ข yt-dlp Engine: โœ… Operational');
          _addToCliOutput('   โ€ข Ready for music operations');
        } else {
          _addToCliOutput('โš ๏ธ  Some components are not ready');
          _addToCliOutput('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”');
          _addToCliOutput('๐Ÿ“Š System Summary:');
          _addToCliOutput(
            '   โ€ข YTMusic API: ${status.ytmusicReady ? 'โœ…' : 'โŒ'} ${status.ytmusicReady ? 'Operational' : 'Failed'}',
          );
          _addToCliOutput(
            '   โ€ข yt-dlp Engine: ${status.ytdlpReady ? 'โœ…' : 'โŒ'} ${status.ytdlpReady ? 'Operational' : 'Failed'}',
          );
          _addToCliOutput('   โ€ข ${status.statusSummary}');
        }
      } else {
        _addToCliOutput('โŒ API Status: Error');
        _addToCliOutput('๐Ÿ“‹ Message: ${response.message ?? 'Unknown status'}');

        if (response.data != null) {
          final status = response.data!;

          _addToCliOutput('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”');
          _addToCliOutput('๐Ÿ“Š Component Status:');

          _addToCliOutput(
            '๐ŸŽต YTMusic: ${status.ytmusicReady ? 'โœ…' : 'โŒ'} ${status.ytmusicReady ? 'Ready' : 'Failed'} (v${status.ytmusicVersion})',
          );

          _addToCliOutput(
            'โฌ‡๏ธ yt-dlp: ${status.ytdlpReady ? 'โœ…' : 'โŒ'} ${status.ytdlpReady ? 'Ready' : 'Failed'} (v${status.ytdlpVersion})',
          );

          _addToCliOutput('๐Ÿ“‹ Status: ${status.statusSummary}');
        }
      }
    } catch (e) {
      _addToCliOutput('โŒ Exception during status check: $e');

      _addToCliOutput('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”');
      _addToCliOutput('๐Ÿ’ก Troubleshooting Guide:');
      _addToCliOutput('   1. Check Python environment setup');
      _addToCliOutput(
        '   2. Verify ytmusicapi installation: pip install ytmusicapi',
      );
      _addToCliOutput('   3. Verify yt-dlp installation: pip install yt-dlp');
      _addToCliOutput('   4. Test network connectivity');
      _addToCliOutput('   5. Check system logs for detailed errors');

      print('Status check error details: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _searchMusic() async {
    if (_isLoading || !_isInitialized) {
      _addToCliOutput('โŒ API not initialized or busy');
      return;
    }

    String query = _searchController.text.trim();
    if (query.isEmpty) {
      query = 'Billie Eilish bad guy';
      _searchController.text = query;
    }

    setState(() {
      _isLoading = true;
    });

    _addToCliOutput('๐Ÿ” Searching for: "$query"');
    _addToCliOutput(
      '๐Ÿ“Š Settings: Limit=${AppSettings.limit}, Audio=${AppSettings.audioQuality.value}, Thumb=${AppSettings.thumbnailQuality.value}, Mode=${AppSettings.mode}',
    );

    try {
      if (AppSettings.mode == 'stream') {
        await _streamSearchResults(query: query);
      } else {
        final response = await _api.searchMusic(
          query: query,
          limit: AppSettings.limit,
          audioQuality: AppSettings.audioQuality,
          thumbQuality: AppSettings.thumbnailQuality,
          includeAudioUrl: true,
          includeAlbumArt: true,
        );

        if (response.success && response.data != null) {
          _addToCliOutput('โœ… Search completed successfully');
          _addToCliOutput('๐Ÿ“‹ Found ${response.data!.length} results');

          for (int i = 0; i < response.data!.length; i++) {
            final result = response.data![i];
            _addToCliOutput('๐ŸŽต Result ${i + 1}:');
            _addToCliOutput('   Title: ${result.title}');
            _addToCliOutput('   Artists: ${result.artists}');
            _addToCliOutput('   Duration: ${result.duration ?? 'N/A'}');
            _addToCliOutput('   Year: ${result.year ?? 'N/A'}');
            _addToCliOutput('   Video ID: ${result.videoId}');
            _addToCliOutput(
              '   Album Art: ${result.albumArt != null ? 'Available and respected!' : 'N/A'}',
            );
            _addToCliOutput(
              '   Audio URL: ${result.audioUrl != null ? 'Available and respected!' : 'N/A'}',
            );
            _addToCliOutput('   ---');
          }

          Inspector.checkRules(response.data!, 'Search Music');
          _addToCliOutput('๐ŸŽ‰ SUCCESS: Search operation completed');
        } else {
          _addToCliOutput('โŒ Search failed');
          _addToCliOutput('๐Ÿ“‹ Error: ${response.error ?? 'Unknown error'}');
        }
      }
    } catch (e) {
      _addToCliOutput('โŒ Exception during search: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _streamSearchResults({required String query}) async {
    int received = 0;
    final stopwatch = Stopwatch()..start();

    _addToCliOutput('๐Ÿ“ก Streaming search results for: "$query"');

    try {
      await for (final result in _api.streamSearchResults(
        query: query,
        limit: AppSettings.limit,
        audioQuality: AppSettings.audioQuality,
        thumbQuality: AppSettings.thumbnailQuality,
        includeAudioUrl: true,
        includeAlbumArt: true,
      )) {
        received++;
        _addToCliOutput('๐ŸŽง Streamed Result $received:');
        _addToCliOutput('   Title: ${result.title}');
        _addToCliOutput('   Artists: ${result.artists}');
        _addToCliOutput('   Duration: ${result.duration ?? 'N/A'}');
        _addToCliOutput('   Video ID: ${result.videoId}');
        _addToCliOutput(
          '   Album Art: ${result.albumArt != null ? 'Available' : 'N/A'}',
        );
        _addToCliOutput(
          '   Audio URL: ${result.audioUrl != null ? 'Available' : 'N/A'}',
        );
        _addToCliOutput('   ---');

        if (received >= AppSettings.limit) {
          _addToCliOutput('โน๏ธ Streaming limit reached (${AppSettings.limit})');
          break;
        }
      }

      _addToCliOutput(
        'โœ… Stream finished: $received result(s) in ${stopwatch.elapsedMilliseconds}ms',
      );
    } catch (e) {
      _addToCliOutput('โŒ Streaming error: $e');
    }
  }

  // Add this method to your _MusicApiTestPageState class
  Future<void> _getRelatedSongs() async {
    if (_isLoading || !_isInitialized) {
      _addToCliOutput('โŒ API not initialized or busy');
      return;
    }

    setState(() {
      _isLoading = true;
    });

    _addToCliOutput(
      '๐Ÿ” Getting related songs for: "${AppSettings.relatedSongTitle}" by ${AppSettings.relatedSongArtist}',
    );
    _addToCliOutput(
      '๐Ÿ“Š Settings: Limit=${AppSettings.limit}, Audio=${AppSettings.audioQuality.value}, '
      'Thumb=${AppSettings.thumbnailQuality.value}, Mode=${AppSettings.mode}',
    );

    try {
      if (AppSettings.mode == 'stream') {
        await _streamRelatedSongs();
      } else {
        final response = await _api.getRelatedSongs(
          songName: AppSettings.relatedSongTitle,
          artistName: AppSettings.relatedSongArtist,
          limit: AppSettings.limit,
          audioQuality: AppSettings.audioQuality,
          thumbQuality: AppSettings.thumbnailQuality,
          includeAudioUrl: true,
          includeAlbumArt: true,
        );

        if (response.success && response.data != null) {
          _addToCliOutput('โœ… Related songs retrieved successfully');
          _addToCliOutput('๐Ÿ“‹ Found ${response.data!.length} related songs');

          for (int i = 0; i < response.data!.length; i++) {
            final song = response.data![i];
            _addToCliOutput('๐ŸŽต Related Song ${i + 1}:');
            _addToCliOutput('   Title: ${song.title}');
            _addToCliOutput('   Artists: ${song.artists}');
            _addToCliOutput('   Duration: ${song.duration ?? 'N/A'}');
            _addToCliOutput('   Is Original: ${song.isOriginal}');
            _addToCliOutput('   Video ID: ${song.videoId}');
            _addToCliOutput(
              '   Album Art: ${song.albumArt != null ? 'Available' : 'N/A'}',
            );
            _addToCliOutput(
              '   Audio URL: ${song.audioUrl != null ? 'Available' : 'N/A'}',
            );
            _addToCliOutput('   ---');
          }

          Inspector.checkRules(response.data!, 'Related Songs');
          _addToCliOutput('๐ŸŽ‰ SUCCESS: Related songs operation completed');
        } else {
          _addToCliOutput('โŒ Failed to get related songs');
          _addToCliOutput('๐Ÿ“‹ Error: ${response.error ?? 'Unknown error'}');
        }
      }
    } catch (e) {
      _addToCliOutput('โŒ Exception during related songs fetch: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _streamRelatedSongs() async {
    int received = 0;
    final stopwatch = Stopwatch()..start();

    try {
      await for (final song in _api.streamRelatedSongs(
        songName: AppSettings.relatedSongTitle,
        artistName: AppSettings.relatedSongArtist,
        limit: AppSettings.limit,
        audioQuality: AppSettings.audioQuality,
        thumbQuality: AppSettings.thumbnailQuality,
        includeAudioUrl: true,
        includeAlbumArt: true,
      )) {
        received++;
        _addToCliOutput(
          '๐ŸŽต Received related song $received in ${stopwatch.elapsedMilliseconds}ms:',
        );
        _addToCliOutput('   Title: ${song.title}');
        _addToCliOutput('   Artists: ${song.artists}');
        _addToCliOutput('   Duration: ${song.duration ?? 'N/A'}');
        _addToCliOutput('   Is Original: ${song.isOriginal}');
        _addToCliOutput('   Video ID: ${song.videoId}');
        _addToCliOutput(
          '   Album Art: ${song.albumArt != null ? 'Available' : 'N/A'}',
        );
        _addToCliOutput(
          '   Audio URL: ${song.audioUrl != null ? 'Available' : 'N/A'}',
        );
        _addToCliOutput('   ---');

        stopwatch.reset();
      }

      _addToCliOutput('โœ… Streamed $received related songs successfully');
    } catch (e) {
      _addToCliOutput('โŒ Exception during related songs stream: $e');
    }
  }

  Future<void> _getArtistSongs() async {
    if (_isLoading || !_isInitialized) {
      _addToCliOutput('โŒ API not initialized or busy');
      return;
    }

    setState(() {
      _isLoading = true;
      _songs.clear();
    });

    _addToCliOutput(
      '๐ŸŽค Getting songs from artist: "${AppSettings.artistName}"',
    );
    _addToCliOutput(
      '๐Ÿ“Š Settings: Limit=${AppSettings.limit}, '
      'Audio=${AppSettings.audioQuality.value}, '
      'Thumb=${AppSettings.thumbnailQuality.value}, '
      'Mode=${AppSettings.mode}',
    );

    try {
      if (AppSettings.mode == 'stream') {
        await _streamArtistSongs();
      } else {
        final response = await _api.getArtistSongs(
          artistName: AppSettings.artistName,
          limit: AppSettings.limit,
          audioQuality: AppSettings.audioQuality,
          thumbQuality: AppSettings.thumbnailQuality,
          includeAudioUrl: true,
          includeAlbumArt: true,
        );

        if (response.success && response.data != null) {
          _addToCliOutput('โœ… Artist songs retrieved successfully');
          _addToCliOutput('๐Ÿ“‹ Found ${response.data!.length} songs');

          for (int i = 0; i < response.data!.length; i++) {
            final song = response.data![i];
            _addToCliOutput('๐ŸŽต Song ${i + 1}:');
            _addToCliOutput('   Title: ${song.title}');
            _addToCliOutput('   Artists: ${song.artists}');
            _addToCliOutput('   Duration: ${song.duration ?? 'N/A'}');
            _addToCliOutput('   Video ID: ${song.videoId}');
            _addToCliOutput('   Artist Name: ${song.artistName}');
            _addToCliOutput(
              '   Album Art: ${song.albumArt != null ? 'Available' : 'N/A'}',
            );
            _addToCliOutput(
              '   Audio URL: ${song.audioUrl != null ? 'Available' : 'N/A'}',
            );
            _addToCliOutput('   ---');

            setState(() {
              _songs.add(song);
            });
          }

          Inspector.checkRules(response.data!, 'Artist Songs');
          _addToCliOutput('๐ŸŽ‰ SUCCESS: Artist songs operation completed');
        } else {
          _addToCliOutput('โŒ Failed to get artist songs');
          _addToCliOutput('๐Ÿ“‹ Error: ${response.error ?? 'Unknown error'}');
        }
      }
    } catch (e) {
      _addToCliOutput('โŒ Exception during artist songs fetch: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _streamArtistSongs() async {
    int received = 0;
    final stopwatch = Stopwatch()..start();

    try {
      await for (final song in _api.streamArtistSongs(
        artistName: AppSettings.artistName,
        limit: AppSettings.limit,
        audioQuality: AppSettings.audioQuality,
        thumbQuality: AppSettings.thumbnailQuality,
        includeAudioUrl: true,
        includeAlbumArt: true,
      )) {
        received++;
        _addToCliOutput(
          '๐ŸŽต Received song $received in ${stopwatch.elapsedMilliseconds}ms:',
        );
        _addToCliOutput('   Title: ${song.title}');
        _addToCliOutput('   Artists: ${song.artists}');
        _addToCliOutput('   Duration: ${song.duration ?? 'N/A'}');
        _addToCliOutput('   Video ID: ${song.videoId}');
        _addToCliOutput('   Artist Name: ${song.artistName}');
        _addToCliOutput(
          '   Album Art: ${song.albumArt != null ? 'Available' : 'N/A'}',
        );
        _addToCliOutput(
          '   Audio URL: ${song.audioUrl != null ? 'Available' : 'N/A'}',
        );
        _addToCliOutput('   ---');

        setState(() {
          _songs.add(song);
        });

        stopwatch.reset();
      }

      if (received > 0) {
        _addToCliOutput('โœ… Streamed $received songs successfully');
        Inspector.checkRules(_songs, 'Artist Songs');
      } else {
        _addToCliOutput('โŒ No songs were streamed');
      }
    } catch (e) {
      _addToCliOutput('โŒ Exception during artist songs stream: $e');
    }
  }

  Future<void> _getSongDetails() async {
    if (_isLoading || !_isInitialized) {
      _addToCliOutput('โŒ API not initialized or busy');
      return;
    }

    setState(() {
      _isLoading = true;
    });

    _addToCliOutput('๐ŸŽต Getting song details');
    _addToCliOutput(
      '๐Ÿ“Š Settings: Mode=${AppSettings.mode}, Audio=${AppSettings.audioQuality.value}, Thumb=${AppSettings.thumbnailQuality.value}',
    );

    try {
      if (AppSettings.mode == 'single') {
        // Handle single song detail using related song inputs
        _addToCliOutput('๐Ÿ” Getting details for single song:');
        _addToCliOutput(
          '   "${AppSettings.relatedSongTitle}" by ${AppSettings.relatedSongArtist}',
        );

        final response = await _api.getSongDetails(
          songs: [
            {
              'title': AppSettings.relatedSongTitle,
              'artist': AppSettings.relatedSongArtist,
            },
          ],
          mode: 'single',
          audioQuality: AppSettings.audioQuality,
          thumbQuality: AppSettings.thumbnailQuality,
          includeAudioUrl: true,
          includeAlbumArt: true,
        );

        if (response.success && response.data != null) {
          final songDetail = response.data as SongDetail;
          _addToCliOutput('โœ… Song details retrieved successfully');
          _addToCliOutput('๐ŸŽต Song Detail:');
          _addToCliOutput('   Title: ${songDetail.title}');
          _addToCliOutput('   Artists: ${songDetail.artists}');
          _addToCliOutput('   Duration: ${songDetail.duration ?? 'N/A'}');
          _addToCliOutput('   Video ID: ${songDetail.videoId}');
          _addToCliOutput('   Year: ${songDetail.year ?? 'N/A'}');
          _addToCliOutput('   Album: ${songDetail.album ?? 'N/A'}');
          _addToCliOutput(
            '   Album Art: ${songDetail.albumArt != null ? 'Available' : 'N/A'}',
          );
          _addToCliOutput(
            '   Audio URL: ${songDetail.audioUrl != null ? 'Available' : 'N/A'}',
          );

          Inspector.checkSingleSongDetail(songDetail, 'Song Details (Single)');
        } else {
          _addToCliOutput('โŒ Failed to get song details');
          _addToCliOutput('๐Ÿ“‹ Error: ${response.error ?? 'Unknown error'}');
        }
      } else if (AppSettings.mode == 'stream') {
        // Handle stream mode with test songs
        _addToCliOutput(
          '๐Ÿ“‹ Streaming details for ${AppSettings.testSongs.length} songs',
        );
        for (int i = 0; i < AppSettings.testSongs.length; i++) {
          final song = AppSettings.testSongs[i];
          _addToCliOutput(
            '   ${i + 1}. "${song['title']}" by ${song['artist']}',
          );
        }

        int received = 0;
        final stopwatch = Stopwatch()..start();

        await for (final response in _api.streamSongDetails(
          songs: AppSettings.testSongs,
          thumbQuality: AppSettings.thumbnailQuality,
          audioQuality: AppSettings.audioQuality,
          includeAudioUrl: true,
          includeAlbumArt: true,
        )) {
          received++;
          if (response.success && response.data != null) {
            final songDetail = response.data as SongDetail;
            _addToCliOutput(
              '๐ŸŽต Received song $received in ${stopwatch.elapsedMilliseconds}ms:',
            );
            _addToCliOutput('   Title: ${songDetail.title}');
            _addToCliOutput('   Artists: ${songDetail.artists}');
            _addToCliOutput('   Duration: ${songDetail.duration ?? 'N/A'}');
            _addToCliOutput('   Video ID: ${songDetail.videoId}');
            _addToCliOutput('   Year: ${songDetail.year ?? 'N/A'}');
            _addToCliOutput('   Album: ${songDetail.album ?? 'N/A'}');
            _addToCliOutput(
              '   Album Art: ${songDetail.albumArt != null ? 'Available' : 'N/A'}',
            );
            _addToCliOutput(
              '   Audio URL: ${songDetail.audioUrl != null ? 'Available' : 'N/A'}',
            );
            _addToCliOutput('   ---');
          } else {
            _addToCliOutput('โŒ Error for song $received');
            _addToCliOutput('   Error: ${response.error ?? 'Unknown error'}');
          }
          stopwatch.reset();
        }

        _addToCliOutput('โœ… Streamed details for $received songs successfully');
      } else {
        // Handle batch mode with test songs
        _addToCliOutput(
          '๐Ÿ“‹ Processing batch of ${AppSettings.testSongs.length} songs',
        );
        for (int i = 0; i < AppSettings.testSongs.length; i++) {
          final song = AppSettings.testSongs[i];
          _addToCliOutput(
            '   ${i + 1}. "${song['title']}" by ${song['artist']}',
          );
        }

        final response = await _api.getSongDetails(
          songs: AppSettings.testSongs,
          mode: 'batch',
          audioQuality: AppSettings.audioQuality,
          thumbQuality: AppSettings.thumbnailQuality,
          includeAudioUrl: true,
          includeAlbumArt: true,
        );

        if (response.success) {
          if (response.data is Map) {
            final data = response.data as Map<String, dynamic>;
            final List<SongDetail> songDetails =
                (data['data'] as List?)?.cast<SongDetail>() ?? [];
            final errors = (data['errors'] as List?) ?? [];
            final processed = data['processed'] as int? ?? 0;
            final errorCount = data['error_count'] as int? ?? 0;

            _addToCliOutput('โœ… Batch processing completed');
            _addToCliOutput('๐Ÿ“‹ Success: $processed, Errors: $errorCount');

            if (songDetails.isNotEmpty) {
              _addToCliOutput('๐ŸŽต Successfully processed songs:');
              for (int i = 0; i < songDetails.length; i++) {
                final song = songDetails[i];
                _addToCliOutput('   ${i + 1}. ${song.title} - ${song.artists}');
                _addToCliOutput('      Video ID: ${song.videoId}');
                _addToCliOutput('      Duration: ${song.duration ?? 'N/A'}');
              }
            }

            if (errors.isNotEmpty) {
              _addToCliOutput('โŒ Errors encountered:');
              for (int i = 0; i < errors.length; i++) {
                final error = errors[i];
                _addToCliOutput('   ${i + 1}. ${error['error']}');
              }
            }

            Inspector.checkRules(songDetails, 'Song Details (Batch)');
          } else if (response.data is List) {
            final songDetails = response.data as List<SongDetail>;
            _addToCliOutput('โœ… Batch processing completed');
            _addToCliOutput('๐Ÿ“‹ Success: ${songDetails.length}');

            for (int i = 0; i < songDetails.length; i++) {
              final song = songDetails[i];
              _addToCliOutput('   ${i + 1}. ${song.title} - ${song.artists}');
              _addToCliOutput('      Video ID: ${song.videoId}');
              _addToCliOutput('      Duration: ${song.duration ?? 'N/A'}');
            }

            Inspector.checkRules(songDetails, 'Song Details (Batch)');
          } else {
            _addToCliOutput('โŒ Invalid response format for batch mode');
          }
        } else {
          _addToCliOutput('โŒ Failed to get song details');
          _addToCliOutput('๐Ÿ“‹ Error: ${response.error ?? 'Unknown error'}');
        }
      }

      _addToCliOutput('๐ŸŽ‰ SUCCESS: Song details operation completed');
    } catch (e) {
      _addToCliOutput('โŒ Exception during song details fetch: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _fetchLyrics() async {
    if (_isLoading || !_isInitialized) {
      _addToCliOutput('โŒ API not initialized or busy');
      return;
    }

    setState(() {
      _isLoading = true;
    });

    // Use the search query if available, otherwise default to "Billie Eilish bad guy"
    String query = _searchController.text.trim();
    if (query.isEmpty) {
      query = 'Billie Eilish bad guy';
      _searchController.text = query;
    }

    // Extract artist and song name (simple parsing for demo)
    String artistName = 'Billie Eilish';
    String songName = 'bad guy';

    try {
      // Try to parse artist and song from query
      final parts = query.split(' ');
      if (parts.length > 1) {
        artistName = parts[0];
        songName = parts.sublist(1).join(' ');
      }

      _addToCliOutput('๐ŸŽต Fetching lyrics for: "$songName" by $artistName');

      final response = await _api.fetchLyrics(
        songName: songName,
        artistName: artistName,
      );

      if (response.success && response.data != null) {
        final lyrics = response.data!;
        _addToCliOutput('โœ… Lyrics fetched successfully');
        _addToCliOutput('๐Ÿ“‹ Source: ${lyrics.source}');
        _addToCliOutput('๐Ÿ“‹ Language: ${lyrics.language ?? 'Unknown'}');
        _addToCliOutput('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”');
        _addToCliOutput('๐ŸŽค LYRICS:');

        // Split lyrics into lines and add each line to CLI output
        final lines = lyrics.text.split('\n');
        for (final line in lines) {
          if (line.trim().isNotEmpty) {
            _addToCliOutput(line);
          }
        }

        _addToCliOutput('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”');
        _addToCliOutput('๐ŸŽ‰ SUCCESS: Lyrics operation completed');
      } else {
        _addToCliOutput('โŒ Failed to fetch lyrics');
        _addToCliOutput('๐Ÿ“‹ Error: ${response.error ?? 'Unknown error'}');
      }
    } catch (e) {
      _addToCliOutput('โŒ Exception during lyrics fetch: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _disposeApi() async {
    if (_isLoading) return;

    setState(() {
      _isLoading = true;
    });

    _addToCliOutput('๐Ÿ—‘๏ธ Disposing API...');

    try {
      final response = await _api.dispose();

      if (response.success) {
        setState(() {
          _isInitialized = false;
        });
        _addToCliOutput('โœ… API Disposed Successfully');
        _addToCliOutput(
          '๐Ÿ“‹ Message: ${response.message ?? 'Resources cleaned up'}',
        );
      } else {
        _addToCliOutput('โŒ API Disposal Failed');
        _addToCliOutput('๐Ÿ“‹ Error: ${response.error ?? 'Unknown error'}');
      }
    } catch (e) {
      _addToCliOutput('โŒ Exception during disposal: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  void _showSettings() {
    showDialog(
      context: context,
      builder: (context) => SettingsDialog(
        onSettingsChanged: () {
          setState(() {});
          _addToCliOutput('โš™๏ธ Settings updated');
        },
        onClearCli: _clearCliOutput, // Add this line
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: AppSettings.isDarkMode ? ThemeData.dark() : ThemeData.light(),
      home: Scaffold(
        appBar: AppBar(
          title: Text('YouTube Music API Test'),
          actions: [
            IconButton(icon: Icon(Icons.settings), onPressed: _showSettings),
          ],
        ),
        body: Column(
          children: [
            // CLI Output Area
            Expanded(
              flex: 3,
              child: Container(
                margin: EdgeInsets.all(8),
                padding: EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: AppSettings.isDarkMode
                      ? Colors.grey[900]
                      : Colors.black,
                  border: Border.all(color: Colors.grey),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Text(
                          'CLI Output',
                          style: TextStyle(
                            color: Colors.green,
                            fontWeight: FontWeight.bold,
                            fontSize: 16,
                          ),
                        ),
                        Spacer(),
                        IconButton(
                          icon: Icon(Icons.clear, color: Colors.green),
                          onPressed: _clearCliOutput,
                          tooltip: 'Clear Output',
                        ),
                      ],
                    ),
                    Divider(color: Colors.green),
                    Expanded(
                      child: ListView.builder(
                        controller: _scrollController,
                        itemCount: _cliOutput.length,
                        itemBuilder: (context, index) {
                          return Padding(
                            padding: EdgeInsets.symmetric(vertical: 1),
                            child: Text(
                              _cliOutput[index],
                              style: TextStyle(
                                color: Colors.green,
                                fontFamily: 'monospace',
                                fontSize: 12,
                              ),
                            ),
                          );
                        },
                      ),
                    ),
                  ],
                ),
              ),
            ),

            // Search Input
            Padding(
              padding: EdgeInsets.all(8),
              child: TextField(
                controller: _searchController,
                decoration: InputDecoration(
                  hintText:
                      'Enter search query (default: Billie Eilish bad guy)',
                  border: OutlineInputBorder(),
                  prefixIcon: Icon(Icons.search),
                ),
              ),
            ),

            // Test Buttons
            Expanded(
              flex: 2,
              child: Container(
                padding: EdgeInsets.all(8),
                child: Column(
                  children: [
                    Text(
                      'Test Controls',
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    SizedBox(height: 8),

                    // Status Row
                    Row(
                      children: [
                        Icon(
                          _isInitialized ? Icons.check_circle : Icons.error,
                          color: _isInitialized ? Colors.green : Colors.red,
                        ),
                        SizedBox(width: 8),
                        Text(
                          _isInitialized
                              ? 'API Initialized'
                              : 'API Not Initialized',
                          style: TextStyle(
                            color: _isInitialized ? Colors.green : Colors.red,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        Spacer(),
                        if (_isLoading)
                          SizedBox(
                            width: 20,
                            height: 20,
                            child: CircularProgressIndicator(strokeWidth: 2),
                          ),
                      ],
                    ),
                    SizedBox(height: 16),

                    // Button Grid
                    // Button Grid
                    Expanded(
                      child: GridView.count(
                        crossAxisCount:
                            3, // Changed from 2 to 3 to accommodate more buttons
                        childAspectRatio: 2.5, // Adjusted aspect ratio
                        crossAxisSpacing: 8,
                        mainAxisSpacing: 8,
                        children: [
                          ElevatedButton.icon(
                            icon: Icon(Icons.play_arrow),
                            label: Text('Initialize'),
                            onPressed: _isLoading ? null : _initializeApi,
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.green,
                              foregroundColor: Colors.white,
                            ),
                          ),
                          ElevatedButton.icon(
                            icon: Icon(Icons.info),
                            label: Text('Check Status'),
                            onPressed: _isLoading ? null : _checkStatus,
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.blue,
                              foregroundColor: Colors.white,
                            ),
                          ),
                          ElevatedButton.icon(
                            icon: Icon(Icons.search),
                            label: Text('Search Music'),
                            onPressed: (_isLoading || !_isInitialized)
                                ? null
                                : _searchMusic,
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.orange,
                              foregroundColor: Colors.white,
                            ),
                          ),
                          ElevatedButton.icon(
                            icon: Icon(Icons.queue_music),
                            label: Text('Related Songs'),
                            onPressed: (_isLoading || !_isInitialized)
                                ? null
                                : _getRelatedSongs,
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.purple,
                              foregroundColor: Colors.white,
                            ),
                          ),
                          ElevatedButton.icon(
                            icon: Icon(Icons.person),
                            label: Text('Artist Songs'),
                            onPressed: (_isLoading || !_isInitialized)
                                ? null
                                : _getArtistSongs,
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.teal,
                              foregroundColor: Colors.white,
                            ),
                          ),
                          ElevatedButton.icon(
                            icon: Icon(Icons.info_outline),
                            label: Text('Song Details'),
                            onPressed: (_isLoading || !_isInitialized)
                                ? null
                                : _getSongDetails,
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.indigo,
                              foregroundColor: Colors.white,
                            ),
                          ),
                          ElevatedButton.icon(
                            icon: Icon(Icons.music_note),
                            label: Text('Fetch Lyrics'),
                            onPressed: (_isLoading || !_isInitialized)
                                ? null
                                : _fetchLyrics,
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.pink,
                              foregroundColor: Colors.white,
                            ),
                          ),
                          ElevatedButton.icon(
                            icon: Icon(Icons.stop),
                            label: Text('Dispose'),
                            onPressed: _isLoading ? null : _disposeApi,
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.red,
                              foregroundColor: Colors.white,
                            ),
                          ),
                          ElevatedButton.icon(
                            icon: Icon(Icons.settings),
                            label: Text('Settings'),
                            onPressed: _showSettings,
                            style: ElevatedButton.styleFrom(
                              backgroundColor: Colors.grey,
                              foregroundColor: Colors.white,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class SettingsDialog extends StatefulWidget {
  final VoidCallback onSettingsChanged;
  final VoidCallback onClearCli; // Add this line

  const SettingsDialog({
    required this.onSettingsChanged,
    required this.onClearCli, // Add this line
    Key? key,
  }) : super(key: key);

  @override
  _SettingsDialogState createState() => _SettingsDialogState();
}

class _SettingsDialogState extends State<SettingsDialog> {
  late int _limit;
  late AudioQuality _audioQuality;
  late ThumbnailQuality _thumbnailQuality;
  late bool _isDarkMode;
  late String _mode;
  late TextEditingController _artistController;

  @override
  void initState() {
    super.initState();
    _limit = AppSettings.limit;
    _audioQuality = AppSettings.audioQuality;
    _thumbnailQuality = AppSettings.thumbnailQuality;
    _isDarkMode = AppSettings.isDarkMode;
    _mode = AppSettings.mode;
    _artistController = TextEditingController(text: AppSettings.artistName);
  }

  @override
  void dispose() {
    _artistController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Settings'),
      content: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // Limit Setting
            ListTile(
              title: Text('Limit: $_limit'),
              subtitle: Slider(
                value: _limit.toDouble(),
                min: 1,
                max: 15,
                divisions: 14,
                onChanged: (value) {
                  setState(() {
                    _limit = value.toInt();
                  });
                },
              ),
            ),

            // Add this to your SettingsDialog build method (inside the Column)
            TextField(
              controller: TextEditingController(
                text: AppSettings.relatedSongTitle,
              ),
              decoration: InputDecoration(
                labelText: 'Related Song Title',
                border: OutlineInputBorder(),
              ),
              onChanged: (value) => AppSettings.relatedSongTitle = value,
            ),
            // Add to your SettingsDialog build method, inside the Column
            TextField(
              controller: TextEditingController(
                text: AppSettings.relatedSongTitle,
              ),
              decoration: InputDecoration(
                labelText: 'Lyrics Song Title',
                border: OutlineInputBorder(),
              ),
              onChanged: (value) => AppSettings.relatedSongTitle = value,
            ),
            SizedBox(height: 8),
            TextField(
              controller: TextEditingController(
                text: AppSettings.relatedSongArtist,
              ),
              decoration: InputDecoration(
                labelText: 'Lyrics Artist Name',
                border: OutlineInputBorder(),
              ),
              onChanged: (value) => AppSettings.relatedSongArtist = value,
            ),
            SizedBox(height: 8),
            TextField(
              controller: TextEditingController(
                text: AppSettings.relatedSongArtist,
              ),
              decoration: InputDecoration(
                labelText: 'Related Song Artist',
                border: OutlineInputBorder(),
              ),
              onChanged: (value) => AppSettings.relatedSongArtist = value,
            ),

            // Mode Selection
            ListTile(
              title: Text('Operation Mode'),
              subtitle: DropdownButton<String>(
                value: _mode,
                isExpanded: true,
                items: ['auto', 'batch', 'stream'].map((mode) {
                  return DropdownMenuItem(
                    value: mode,
                    child: Text(mode.toUpperCase()),
                  );
                }).toList(),
                onChanged: (value) {
                  if (value != null) {
                    setState(() {
                      _mode = value;
                    });
                  }
                },
              ),
            ),

            // Artist Name Input
            TextField(
              controller: _artistController,
              decoration: InputDecoration(
                labelText: 'Artist Name for Testing',
                border: OutlineInputBorder(),
              ),
            ),
            SizedBox(height: 8),

            // Audio Quality Setting
            ListTile(
              title: Text('Audio Quality'),
              subtitle: DropdownButton<AudioQuality>(
                value: _audioQuality,
                isExpanded: true,
                items: AudioQuality.values.map((quality) {
                  return DropdownMenuItem(
                    value: quality,
                    child: Text(quality.value),
                  );
                }).toList(),
                onChanged: (value) {
                  if (value != null) {
                    setState(() {
                      _audioQuality = value;
                    });
                  }
                },
              ),
            ),

            // Thumbnail Quality Setting
            ListTile(
              title: Text('Thumbnail Quality'),
              subtitle: DropdownButton<ThumbnailQuality>(
                value: _thumbnailQuality,
                isExpanded: true,
                items: ThumbnailQuality.values.map((quality) {
                  return DropdownMenuItem(
                    value: quality,
                    child: Text(quality.value),
                  );
                }).toList(),
                onChanged: (value) {
                  if (value != null) {
                    setState(() {
                      _thumbnailQuality = value;
                    });
                  }
                },
              ),
            ),
            SizedBox(height: 16),
            ElevatedButton.icon(
              icon: Icon(Icons.clear),
              label: Text('Clear CLI Output'),
              onPressed: () {
                widget.onClearCli();
                Navigator.of(context).pop();
              },
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.green,
                foregroundColor: Colors.blueGrey,
              ),
            ),

            // Dark Mode Toggle
            SwitchListTile(
              title: Text('Dark Mode'),
              value: _isDarkMode,
              onChanged: (value) {
                setState(() {
                  _isDarkMode = value;
                });
              },
            ),
          ],
        ),
      ),

      actions: [
        TextButton(
          onPressed: () {
            Navigator.of(context).pop();
          },
          child: Text('Cancel'),
        ),
        ElevatedButton(
          onPressed: () {
            // Save settings
            AppSettings.limit = _limit;
            AppSettings.audioQuality = _audioQuality;
            AppSettings.thumbnailQuality = _thumbnailQuality;
            AppSettings.isDarkMode = _isDarkMode;
            AppSettings.mode = _mode;
            AppSettings.artistName = _artistController.text.trim();

            widget.onSettingsChanged();
            Navigator.of(context).pop();
          },
          child: Text('Save'),
        ),
      ],
    );
  }
}
5
likes
0
points
155
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for YouTube Music API that provides music search and related songs functionality using Python backend.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on yt_flutter_musicapi

Packages that implement yt_flutter_musicapi