# How to Show Live YouTube Music Data on your Website

Spotify and Apple Music make it incredibly easy to showcase your live listening habits on your website. YouTube Music? Not so much. After doing some setup you can do an API call and fetch your account's playing data but YouTube does not support any official APIs for this purpose. So I decided to build my own. In theory this works for any music app, even Spotify. Check out what I'm listening to at [himanshubalani.com](https://himanshubalani.com/#/about) .

Hey but there are [unofficial Chrome extensions](https://github.com/drimescodes/ytm-extension) you can use right? Right, but I use the app on my **Android Phone.** So I decided to build one for myself.

I develop apps in Flutter so I'll use it in this guide. But feel free to ask an AI agent of your choice to build the same for you in Kotlin or React Native taking this code as reference.

## What we will build today

*   A flutter app that listens to your music playing activity
    
*   A firebase Realtime data service
    

## Prerequisites

*   Basic knowledge of Flutter - [Install](https://docs.flutter.dev/install) and [create new app](https://docs.flutter.dev/reference/create-new-app)
    
*   Knowledge about [Android Studio](https://developer.android.com/studio) or IDE of choice
    
*   What is [Firebase](https://firebase.google.com/)
    

## Concepts

Since YouTube Music does not have any developer APIs we are going to use a workaround. When we play any media on our phone, there is always a notification stating what media is playing and from which application along with some controls like play/pause, next/previous. In our workaround, we will listen to this information and broadcast it to our real-time database and then we can fetch that data into our website or use it wherever we like.

**Understanding the Notification Data**  
Before we jump into the code, let's break down what an Android media notification actually contains. When the Flutter package intercepts a notification, it gives us an event object with several key pieces of information. Here are the 5 parts we care about:

1.  **packageName**: This is the unique identifier of the app that posted the notification (e.g., [com.google.android.apps.youtube.music](http://com.google.android.apps.youtube.music)). We use this to filter out irrelevant notifications like messages or emails or updates so we *only* track our music player.
    
2.  **title**: In a media notification, the title almost always maps to the **Song Name**.
    
3.  **content**: The body text of the notification. For music apps, this usually contains the **Artist Name** (and sometimes the album name).
    
4.  **largeIcon**: This is the image attached to the notification, which represents the **Album Art**. Because it is sent through the Android system tray, it is heavily compressed and down sampled as a byte array (Uint8List).
    
5.  **hasRemoved**: A boolean (true/false) flag. If this is true, it means the notification was swiped away or the media session ended, which tells our app that the music has **stopped playing**.
    

## Getting Started

Create a new Flutter app using your terminal or IDE:

```shell
flutter create yt_music_tracker
cd yt_music_tracker
```

### Step 1: Install Dependencies

Open your `pubspec.yaml` file and add the required packages. We will need tools for Firebase, listening to notifications, and making HTTP requests. Run this in CLI:

```shell
flutter pub add firebase_core firebase_database firebase_auth notification_listener_service http url_launcher
```

### Step 2: Configure Android Permissions

Since Flutter needs to talk to the native Android OS to read notifications, we have to declare a special service in our Android manifest.

Open `android/app/src/main/AndroidManifest.xml` and add this `<service>` block inside the `<application>` tag:

```xml
<service android:label="notifications" 
    android:name="notification.listener.service.NotificationListener" 
    android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" 
    android:exported="true">
    <intent-filter>
        <action android:name="android.service.notification.NotificationListenerService" />
    </intent-filter>
</service>
```

Check out this [GitHub Gist](https://gist.github.com/himanshubalani/aa13b07ab43c0720e0188bc65564e819) to see and example of Android Manifest file and other code files.

### Step 3: Set up Project in Firebase Console

1.  Go to the [Firebase Console](https://console.firebase.google.com/) and create a new project. Follow the instructions. In the end you may be asked to download `google_services.json`. Place the file in `android/app` folder.
    
2.  Go to Console > Product Categories > Databases and Storage > Realtime Database. Enable **Realtime Database** in the Firebase console.
    
3.  Enable **Anonymous Authentication** in Firebase Auth ( Console > Product Categories > Security > Authentication > Sign-in method > Add Provider). This ensures our app can securely write to the database without needing a full email/password login.
    
4.  Update your Realtime Database Rules to allow your authenticated app to write, but anyone to read (so your website can fetch it). `live_music` is our endpoint at which data will be sent (or written) and then anyone can read from it (like your website).
    

```json
{
  "rules": {
    "live_music": {
      ".read": true,
      ".write": "auth != null"
    }
  }
}
```

### Step 4: Setup Firebase in Flutter app

These directions may change depending on when you're reading this blog. So always follow [**Get started with Firebase in your Flutter project**](https://firebase.google.com/docs/flutter/setup#android) docs. Follow the instructions given here. Make sure to add `firebase_auth` and `firebase_database` plugins in the final step. You'll have a `firebase_options.dart` with your app data in the end of this step.

### Step 5: The Flutter App - Initialization

Open your `main.dart` file. We need to initialize Firebase and sign in anonymously before the app runs. Check out this [GitHub Gist](https://gist.github.com/himanshubalani/aa13b07ab43c0720e0188bc65564e819) for full code.

```dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  await FirebaseAuth.instance.signInAnonymously(); 
  runApp(const YTMusicTrackerApp());
}

class YTMusicTrackerApp extends StatelessWidget {
  const YTMusicTrackerApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'YT Music Tracker',
      theme: ThemeData.dark(useMaterial3: true),
      home: const TrackerScreen(),
    );
  }
}
```

### Step 6: Listening to Notifications

Next, we'll create the `TrackerScreen` widget. It needs to check if the user has granted "Notification Access" permission, and if so, start listening to the stream of incoming notifications.

To make sure we only track music, we will filter the notifications by the package name. (You can add as many Music apps you like, it is not limited to YouTube Music). This will only track music apps and not any other apps. Just search for package names on the web for your apps or look in app info in your phone.

```dart
class TrackerScreen extends StatefulWidget {
  const TrackerScreen({super.key});

  @override
  State<TrackerScreen> createState() => _TrackerScreenState();
}

class _TrackerScreenState extends State<TrackerScreen> {
  bool _hasPermission = false;
  ServiceNotificationEvent? _currentTrack;

  final List<String> ytMusicPackages = [
    'com.google.android.apps.youtube.music', // Official YT Music
    'au.com.shiftyjelly.pocketcasts', // PocketCasts
   //You can add more here
  ];

  @override
  void initState() {
    super.initState();
    _checkPermissionAndListen();
  }

  Future<void> _checkPermissionAndListen() async {
    bool isGranted = await NotificationListenerService.isPermissionGranted();
    setState(() => _hasPermission = isGranted);
    if (isGranted) _startListening();
  }

  Future<void> _requestPermission() async {
    bool isGranted = await NotificationListenerService.requestPermission();
    setState(() => _hasPermission = isGranted);
    if (isGranted) _startListening();
  }
  
  // ... more code to come
}
```

### Step 7: The High-Res Album Art Hack

Here is where things get interesting. When Android passes a notification to our app, the album art attached to it (`largeIcon`) is heavily compressed to save memory. It looks pixelated and terrible on a website.

**The Fix:** We take the Song Title and Artist Name from the notification and make a HTTP request to the free iTunes Search API. It returns ultra-high-resolution album art!

```dart
  Future<String?> fetchHighResAlbumArt(String title, String artist) async {
    try {
      final cleanTitle = title.split('(')[0].trim();
      final query = Uri.encodeComponent('$cleanTitle $artist');
      final url = Uri.parse('https://itunes.apple.com/search?term=$query&entity=song&limit=1');

      final response = await http.get(url);
      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        if (data['results'] != null && data['results'].isNotEmpty) {
          String artUrl = data['results'][0]['artworkUrl100'];
          return artUrl.replaceAll('100x100bb.jpg', '1000x1000bb.jpg');
        }
      }
    } catch (e) {
      debugPrint("Failed to fetch high-res art: $e");
    }
    return null;
  }
```

We can also extract the **dominant color** of the album art so our website can adapt its theme dynamically. I've noticed inversePrimary always creates colors that contrast better with text, so I'm using that.

```dart
  Color? _dominantColor;

  Future<void> _getDominantColor(String imageUrl) async {
    try {
      final imageProvider = NetworkImage(imageUrl);
      final colorScheme = await ColorScheme.fromImageProvider(provider: imageProvider);
      setState(() {
        _dominantColor = colorScheme.inversePrimary;
      });
    } catch (e) {
      setState(() => _dominantColor = Colors.redAccent);
    }
  }
```

### Step 8: Sending the Listener Data to Firebase

Now, let's write the core `_startListening()` logic. Whenever a notification arrives, we:

1.  Check if it belongs to our target apps.
    
2.  Fetch the high-res art.
    
3.  Extract the dominant color.
    
4.  Push all of this to Firebase Realtime Database.
    

```plaintext
  String? _lastUploadedTitle;
  DateTime? _lastFirebaseUpdate;
  String? _highResArtUrl;

  void _startListening() {
    NotificationListenerService.notificationsStream.listen((event) async {
      if (ytMusicPackages.contains(event.packageName)) {
        if (event.hasRemoved ?? false) {
          // Music paused or app swiped away
          setState(() {
            _currentTrack = null;
            _highResArtUrl = null;
            _dominantColor = Colors.redAccent;
          });
          FirebaseDatabase.instance.ref("live_music").update({"isPlaying": false});
        } else {
          // Music Playing!
          setState(() {
            _currentTrack = event;
            _highResArtUrl = null;
          });

          if (event.title != null && event.content != null) {
            String? url = await fetchHighResAlbumArt(event.title!, event.content!);

            if (_currentTrack?.title == event.title) {
              setState(() => _highResArtUrl = url);

              if (url != null) {
                await _getDominantColor(url);
              } else {
                setState(() => _dominantColor = Colors.redAccent); 
              }

              updateLiveMusicOnFirebase(
                event.title!,
                event.content!,
                url,
                true,
                _dominantColor,
              );
            }
          }
        }
      }
    });
  }

  void updateLiveMusicOnFirebase(String title, String artist, String? artUrl, bool isPlaying, Color? dominantColor) {
    final now = DateTime.now();
    
    // Prevent spamming Firebase by checking if it's the exact same song updated recently
    final isSameSong = _lastUploadedTitle == title;
    final isTooSoon = _lastFirebaseUpdate != null && now.difference(_lastFirebaseUpdate!).inMinutes < 2;

    if (isSameSong && isTooSoon) return;

    _lastFirebaseUpdate = now;
    _lastUploadedTitle = title;

    final colorHex = dominantColor != null
        ? '#${dominantColor.toARGB32().toRadixString(16).toUpperCase()}'
        : null;

    DatabaseReference ref = FirebaseDatabase.instance.ref("live_music");
    ref.set({
      "title": title,
      "artist": artist,
      "albumArt": artUrl,
      "isPlaying": isPlaying,
      "updatedAt": now.toIso8601String(),
      "albumColor": colorHex,
    });
  }
```

Notice how I added a limit to sending data?

*   If you play/pause the **same song**, it blocks the update from sending again if it has been less than 2 minutes.
    
*   If a **next song** plays, `isSameSong` becomes false, which instantly bypasses the rate limit and sends the new song data immediately.
    

*Note: For the UI code (*`_buildBody`*), you can render simple widgets showing* `_currentTrack.title`*,* `_currentTrack.content` *(Artist), and an* `Image.network` *using our* `_highResArtUrl`*. Feel free to get creative with your Flutter UI! I just used a simple column layout.*

> Check out this [GitHub Gist](https://gist.github.com/himanshubalani/aa13b07ab43c0720e0188bc65564e819) to see complete code files and a sample HTML website with UI to see an example. Remember to change the FIREBASE\_URL in gist with your firebase database URL.

## How to use this on your Website

The beauty of Firebase Realtime Database is that **any database path instantly becomes a standard JSON REST API** just by adding `.json` to the end of the URL. You can find your URL in Firebase Console > Realtime Database > Data.

You don't need heavy Firebase Web SDKs on your portfolio. You can just do a standard HTTP fetch from your React, Vue, or Flutter Web app:

```javascript
// Fetch this URL on your website
const response = await fetch('https://YOUR-PROJECT-ID-default-rtdb.firebaseio.com/live_music.json');
const musicData = await response.json();

console.log(`Now playing: ${musicData.title} by ${musicData.artist}`);
// Use musicData.albumColor to dynamically style your website's UI!
```

#### Building the Pill Widget

Fetching the JSON is only half the story, the fun part is turning it into something that actually looks alive on your website. I wanted a pill shape on my About page, it shows the album art, the song title and the artist with dynamic BG Color.

Here's a simple HTML/CSS/JS codeblock for it same idea as the pills you saw in the cover image above. It reads title, artist, albumArt, and albumColor straight from the Firebase endpoint and updates itself every 15 seconds.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Now playing — neobrutal</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;600;700&family=Roboto+Flex:wght@800;900&display=swap" rel="stylesheet">
<style>
  *{box-sizing:border-box;}
  body{
    margin:0;
    min-height:100vh;
    background:#f2f0e9;
    font-family:'Quicksand',sans-serif;
    display:flex;
    flex-direction:column;
    align-items:center;
    justify-content:center;
    gap:48px;
    padding:56px 20px;
  }
  .group{ display:flex; flex-direction:column; align-items:center; gap:12px; }
  .group-label{
    font-family:'Roboto Flex',sans-serif;
    font-weight:800;
    font-size:11px;
    letter-spacing:0.14em;
    text-transform:uppercase;
    color:#111;
    opacity:0.45;
  }

  .music-icon{ display:block; }
  .box-circles{
  display:flex;
  align-items:right;
  gap:2px;
}

.box-circles span{
  width:15px;
  height:15px;
  border-radius:50%;
  border:2px solid #111;
  background:rgba(255,255,255,0.8);
  flex-shrink:0;
  box-sizing:border-box;
}

  /* ---- mobile card ---- */
  .nb-card{
    width:340px;
    height:104px;
    border-radius:18px;
    border:2.5px solid #111;
    display:flex;
    overflow:hidden;
    background:#1f7a8c;
    color:#fff;
    box-shadow:0px 0px 0 rgba(17,17,17,0.9);
    transition:background 0.3s ease, color 0.3s ease;
  }
  .nb-art{
    width:104px;
    height:100%;
    object-fit:cover;
    flex:0 0 auto;
    background:#0003;
    border-right:2.5px solid #111;
    display:block;
  }
  .nb-body{
    flex:1 1 auto;
    min-width:0;
    padding:9px 12px;
    display:flex;
    flex-direction:column;
    justify-content:space-between;
  }
  .nb-top-row{ display:flex; align-items:flex-start; justify-content:space-between; gap:8px; }
  .nb-eyebrow{
    font-weight:700;
    font-size:11px;
    letter-spacing:0.02em;
    white-space:nowrap;
    overflow:hidden;
    text-overflow:ellipsis;
  }
  .nb-title{
    font-weight:700;
    font-size:14px;
    line-height:1.25;
    margin:0;
    display:-webkit-box;
    -webkit-line-clamp:2;
    -webkit-box-orient:vertical;
    overflow:hidden;
  }
  .nb-artist{
    font-weight:600;
    font-size:11px;
    margin:2px 0 0;
    white-space:nowrap;
    overflow:hidden;
    text-overflow:ellipsis;
    opacity:0.85;
  }
  .nb-bottom-row{ display:flex; align-items:center; justify-content:flex-end; gap:5px; }
  .nb-bottom-row .music-label{
    font-family:'Roboto Flex',sans-serif;
    font-weight:900;
    font-size:12px;
    letter-spacing:0.02em;
  }

  /* ---- desktop pill ---- */
  .nb-pill{
    width:min(92vw,480px);
    border-radius:16px;
    border:2.5px solid #111;
    background:#1f7a8c;
    color:#111;
    display:flex;
    align-items:center;
    gap:6px;
    padding:4px 8px;
    box-shadow:0px 0px 0 rgba(17,17,17,0.9);
    transition:background 0.3s ease, color 0.3s ease;
  }
  .nb-pill .music-icon{ flex:0 0 auto; }
  .nb-listening{
    font-weight:700;
    font-size:13px;
    white-space:nowrap;
    flex:0 0 auto;
  }
  .nb-thumb{
    width:30px; height:30px; border-radius:8px;
    object-fit:cover;
    border:1.5px solid currentColor;
    flex:0 0 auto;
  }
  .nb-track{
    font-weight:700;
    font-size:13px;
    white-space:nowrap;
    overflow:hidden;
    text-overflow:ellipsis;
    flex:1 1 auto;
    min-width:0;
  }
  .nb-pill .box-circles{ flex:0 0 auto; margin-left:4px; }

  .nb-card[data-state="loading"] .nb-title,
  .nb-pill[data-state="loading"] .nb-track{
    background:rgba(255,255,255,0.3);
    border-radius:4px;
    color:transparent;
  }
</style>
</head>
<body>

<div class="group">
  <span class="group-label">Mobile</span>
  <div class="nb-card" id="nbCard" data-state="loading">
    <img class="nb-art" id="nbArt" src="" alt="">
    <div class="nb-body">
<div class="nb-top-row">
  <span class="nb-eyebrow" id="nbEyebrow">Listening to</span>

  <div class="box-circles">
    <span></span>
  </div>
</div>
      <div>
        <p class="nb-title" id="nbTitle">Loading&hellip;</p>
        <p class="nb-artist" id="nbArtist">&nbsp;</p>
      </div>
      <div class="nb-bottom-row">
        <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="20" height="20" viewBox="0 0 50 50">
<path d="M 25 3 C 12.85 3 3 12.85 3 25 C 3 37.15 12.85 47 25 47 C 37.15 47 47 37.15 47 25 C 47 12.85 37.15 3 25 3 z M 25 11 C 32.72 11 39 17.28 39 25 C 39 32.72 32.72 39 25 39 C 17.28 39 11 32.72 11 25 C 11 17.28 17.28 11 25 11 z M 25 13 C 18.383 13 13 18.383 13 25 C 13 31.617 18.383 37 25 37 C 31.617 37 37 31.617 37 25 C 37 18.383 31.617 13 25 13 z M 22.019531 18.501953 C 22.194031 18.505203 22.366984 18.552484 22.521484 18.646484 L 31.521484 24.146484 C 31.817484 24.327484 32 24.651 32 25 C 32 25.349 31.818484 25.671516 31.521484 25.853516 L 22.521484 31.353516 C 22.361484 31.450516 22.181 31.5 22 31.5 C 21.832 31.5 21.663719 31.456094 21.511719 31.371094 C 21.195719 31.194094 21 30.861 21 30.5 L 21 19.5 C 21 19.139 21.195719 18.805906 21.511719 18.628906 C 21.670219 18.540906 21.845031 18.498703 22.019531 18.501953 z"></path>
</svg>
        <span class="music-label">Music</span>
      </div>
    </div>
  </div>
</div>

<div class="group">
  <span class="group-label">Desktop</span>
  <div class="nb-pill" id="nbPill" data-state="loading">
    <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="20" height="20" viewBox="0 0 50 50">
<path d="M 25 3 C 12.85 3 3 12.85 3 25 C 3 37.15 12.85 47 25 47 C 37.15 47 47 37.15 47 25 C 47 12.85 37.15 3 25 3 z M 25 11 C 32.72 11 39 17.28 39 25 C 39 32.72 32.72 39 25 39 C 17.28 39 11 32.72 11 25 C 11 17.28 17.28 11 25 11 z M 25 13 C 18.383 13 13 18.383 13 25 C 13 31.617 18.383 37 25 37 C 31.617 37 37 31.617 37 25 C 37 18.383 31.617 13 25 13 z M 22.019531 18.501953 C 22.194031 18.505203 22.366984 18.552484 22.521484 18.646484 L 31.521484 24.146484 C 31.817484 24.327484 32 24.651 32 25 C 32 25.349 31.818484 25.671516 31.521484 25.853516 L 22.521484 31.353516 C 22.361484 31.450516 22.181 31.5 22 31.5 C 21.832 31.5 21.663719 31.456094 21.511719 31.371094 C 21.195719 31.194094 21 30.861 21 30.5 L 21 19.5 C 21 19.139 21.195719 18.805906 21.511719 18.628906 C 21.670219 18.540906 21.845031 18.498703 22.019531 18.501953 z"></path>
</svg>
    <span class="nb-listening">listening to</span>
    <img class="nb-thumb" id="nbThumb" src="" alt="">
    <span class="nb-track" id="nbTrack">Loading&hellip;</span>
    <div class="box-circles">
  <span></span>
  <span></span>
  <span></span>
</div>
  </div>
</div>

<script>
  const FIREBASE_URL = "https://yt-music-tracker-default-rtdb.asia-southeast1.firebasedatabase.app/live_music.json"; //Replace this with your URL
  const POLL_MS = 15000;

  const nbCard = document.getElementById('nbCard');
  const nbEyebrow = document.getElementById('nbEyebrow');
  const nbArt = document.getElementById('nbArt');
  const nbTitle = document.getElementById('nbTitle');
  const nbArtist = document.getElementById('nbArtist');

  const nbPill = document.getElementById('nbPill');
  const nbThumb = document.getElementById('nbThumb');
  const nbTrack = document.getElementById('nbTrack');

  function parseArgb(hex){
    if(!hex || typeof hex !== 'string') return null;
    const clean = hex.replace('#','');
    if(clean.length === 8){
      return { r: parseInt(clean.slice(2,4),16), g: parseInt(clean.slice(4,6),16), b: parseInt(clean.slice(6,8),16) };
    }
    if(clean.length === 6){
      return { r: parseInt(clean.slice(0,2),16), g: parseInt(clean.slice(2,4),16), b: parseInt(clean.slice(4,6),16) };
    }
    return null;
  }
  function luminance(r,g,b){ return (0.299*r + 0.587*g + 0.114*b) / 255; }

  function applyBg(el, argbHex){
    const c = parseArgb(argbHex) || { r:31, g:122, b:140 };
    const bg = 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')';
    const textColor = luminance(c.r,c.g,c.b) < 0.5 ? '#ffffff' : '#111111';
    el.style.background = bg;
    el.style.color = textColor;
  }

  function escapeHtml(s){
    return String(s).replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
  }

  async function refresh(){
    try{
      const res = await fetch(FIREBASE_URL, {cache:'no-store'});
      if(!res.ok) throw new Error('bad response');
      const data = await res.json();
      if(!data){ throw new Error('no data'); }

      const t = data.title || 'Untitled';
      const a = data.artist || 'Unknown artist';

      nbCard.dataset.state = 'ok';
      applyBg(nbCard, data.albumColor);
      nbEyebrow.textContent = data.isPlaying ? 'Listening to' : 'Last played';
      if(nbArt.src !== data.albumArt){ nbArt.src = data.albumArt || ''; }
      nbTitle.textContent = t;
      nbArtist.textContent = a;

      nbPill.dataset.state = 'ok';
      applyBg(nbPill, data.albumColor);
      if(nbThumb.src !== data.albumArt){ nbThumb.src = data.albumArt || ''; }
      nbTrack.textContent = t + ' by ' + a;
    }catch(err){
      nbCard.dataset.state = 'error';
      nbTitle.textContent = 'Can\u2019t reach tracker';
      nbArtist.textContent = 'Retrying\u2026';
      nbPill.dataset.state = 'error';
      nbTrack.textContent = 'Can\u2019t reach tracker \u2014 retrying\u2026';
    }
  }

  refresh();
  setInterval(refresh, POLL_MS);
</script>

</body>
</html>
```

## Conclusion

And there you have it! A completely custom, free pipeline to broadcast your YouTube Music listening habits directly to your personal website. By combining Android's powerful Notification API with a clever iTunes search query, we bypassed the lack of an official Google API to get high-quality, real-time data.

Install the app on your phone, grant it notification access, throw on your favorite playlist, and share it with the world.

### Bonus

Here are some features you can implement easily with AI. Put pubspec.yaml file and our main.dart file in an AI agent and you can get these features on one prompt:

1.  Native Android. Port to Kotlin. Ask AI to build this in Kotlin for better battery life.
    
2.  Add Background Service. Right now the app runs for a few minutes/hours and stops due to how android treats open apps and limited memory space. Ask AI to add `flutter_background_service` to this app so it runs all the time in the background.
    
3.  Add package\_selector. Add a dropdown in the Flutter app to select which apps are tracked for music notifications.
