diff --git a/.gitignore b/.gitignore
index d80f78d..9299da2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,11 @@ coverage/
 .flaskenv*
 !.env.project
 !.env.vault
-.history
\ No newline at end of file
+.history
+
+/shazam_api/lib/
+/shazam_api/include/
+/shazam_api/__pycache__
+/shazam_api/pyvenv.cfg
+/shazam_api/pyvenv.cfg
+.DS_Store
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 8c6d5d8..4ad2bd4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,8 +4,10 @@ ARG dart_entryfile
 
 WORKDIR /app
 COPY pubspec.* /app/
+COPY shazam_client /app/
 RUN dart pub get
 
+COPY . /app
 COPY . /app
 RUN dart pub get
 
diff --git a/bin/radio_horizon_development.dart b/bin/radio_horizon_development.dart
index b9e552d..1b10464 100644
--- a/bin/radio_horizon_development.dart
+++ b/bin/radio_horizon_development.dart
@@ -47,7 +47,6 @@ Future<void> main() async {
   // Initialise our services
   MusicService.init(client);
   await DatabaseService.init(client);
-  SongRecognitionService.init(client, DatabaseService.instance);
 
   client.onReady.listen((_) async {
     BootUpService.init(client, DatabaseService.instance);
diff --git a/bin/radio_horizon_production.dart b/bin/radio_horizon_production.dart
index 6eb62b8..011ecd7 100644
--- a/bin/radio_horizon_production.dart
+++ b/bin/radio_horizon_production.dart
@@ -62,7 +62,6 @@ Future<void> main() async {
     // Initialise our services
     MusicService.init(client);
     await DatabaseService.init(client);
-    SongRecognitionService.init(client, DatabaseService.instance);
     BootUpService.init(client, DatabaseService.instance);
 
     // Connect
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index 70184bf..772cf11 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -9,8 +9,10 @@ services:
      - .env/.env.production
     links:
       - lavalink
+      - shazam_api
     depends_on:
       - lavalink
+      - shazam_api
 
   lavalink:
     image: ghcr.io/lavalink-devs/lavalink:3
@@ -20,3 +22,13 @@ services:
       - 2333
     volumes:
      - ./lavalink.yml:/opt/Lavalink/application.yml
+
+  shazam_api:
+    build:
+      context: ./shazam_api
+    expose:
+      - 5000
+    volumes:
+      - ./shazam_api:/app
+    environment:
+      FLASK_ENV: production
diff --git a/docker-compose.yml b/docker-compose.yml
index 96fa69a..be5fb22 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,8 +12,10 @@ services:
      - .env/.env.development
     links:
       - lavalink
+      - shazam_api
     depends_on:
       - lavalink
+      - shazam_api
 
   lavalink:
     image: ghcr.io/lavalink-devs/lavalink:3
@@ -23,3 +25,13 @@ services:
       - 2333
     volumes:
      - ./lavalink.yml:/opt/Lavalink/application.yml
+
+  shazam_api:
+    build:
+      context: ./shazam_api
+    expose:
+      - 5000
+    volumes:
+      - ./shazam_api:/app
+    environment:
+      FLASK_ENV: development
diff --git a/lib/src/commands/music.dart b/lib/src/commands/music.dart
index 253a3fc..9ebf013 100644
--- a/lib/src/commands/music.dart
+++ b/lib/src/commands/music.dart
@@ -106,7 +106,7 @@ ChatCommand:music-play: {
           );
         }
 
-        await SongRecognitionService.instance
+        await DatabaseService.instance
             .deleteRadioFromList(context.guild!.id);
       }),
       localizedDescriptions: localizedValues(
diff --git a/lib/src/commands/radio.dart b/lib/src/commands/radio.dart
index dce52de..cff463e 100644
--- a/lib/src/commands/radio.dart
+++ b/lib/src/commands/radio.dart
@@ -12,12 +12,12 @@ import 'package:logging/logging.dart';
 import 'package:nyxx/nyxx.dart';
 import 'package:nyxx_commands/nyxx_commands.dart';
 import 'package:nyxx_interactions/nyxx_interactions.dart';
-import 'package:nyxx_pagination/nyxx_pagination.dart';
 import 'package:radio_browser_api/radio_browser_api.dart';
 import 'package:radio_horizon/radio_horizon.dart';
 import 'package:radio_horizon/src/checks.dart';
 import 'package:radio_horizon/src/models/song_recognition/current_station_info.dart';
 import 'package:retry/retry.dart';
+import 'package:shazam_client/shazam_client.dart';
 
 final _enRadioCommand = AppLocale.en.translations.commands.radio;
 final _enPlayCommand = _enRadioCommand.children.play;
@@ -124,7 +124,8 @@ ChatCommand:radio-play: {
             channelId: context.channel.id,
           ).startPlaying();
 
-        await SongRecognitionService.instance.setCurrentRadio(
+        final databaseService = DatabaseService.instance;
+        await databaseService.setCurrentRadio(
           context.guild!.id,
           context.member!.voiceState!.channel!.id,
           context.channel.id,
@@ -158,39 +159,18 @@ ChatCommand:radio-play: {
         final translations = getCommandTranslations(context);
         final commandTranslations = translations.radio.children.recognize;
         CurrentStationInfo? stationInfo;
-        MusicLinksResponse? linksResponse;
 
         try {
           final recognitionService = SongRecognitionService.instance;
+          final databaseService = DatabaseService.instance;
           final guildId = context.guild!.id;
 
-          final stopwatch = Stopwatch()..start();
           var recognitionSampleDuration = 10;
 
-          final guildRadio = await recognitionService.currentRadio(guildId);
+          final guildRadio = await databaseService.currentRadio(guildId);
 
           try {
-            final info = await retry(
-              () async => recognitionService.getCurrentStationInfo(guildRadio),
-            );
-            if (!info.hasTitle) {
-              throw Exception('No title');
-            }
-            final node = MusicService.instance.cluster
-                .getOrCreatePlayerNode(context.guild!.id);
-            final tracks = await node.autoSearch(info.title!);
-            stationInfo = info.copyWith(
-              image:
-                  'https://img.youtube.com/vi/${tracks.tracks.first.info?.identifier}/hqdefault.jpg',
-            );
-          } catch (exception, stacktrace) {
-            _logger.severe(
-              'Failed to get current station info',
-              exception,
-              stacktrace,
-            );
-
-            ShazamResult? result;
+            SongModel? result;
             await retry(
               () async {
                 result = await recognitionService.identify(
@@ -219,21 +199,18 @@ ChatCommand:radio-play: {
 
             stationInfo =
                 CurrentStationInfo.fromShazamResult(result!, guildRadio);
-          }
-
-          try {
-            linksResponse = await SongRecognitionService.instance
-                .getMusicLinks(stationInfo.title!);
-          } catch (exception, stacktrace) {
-            _logger.severe(
-              'Failed to get music links for ${stationInfo.title}',
-              exception,
-              stacktrace,
+          } catch (e) {
+            await context.respond(
+              MessageBuilder.embed(
+                EmbedBuilder()
+                  ..color = DiscordColor.red
+                  ..title = commandTranslations.errors.noResults,
+              ),
             );
+            return null;
           }
 
           final color = getRandomColor();
-          stopwatch.stop();
 
           final embed = EmbedBuilder()
             ..color = color
@@ -245,7 +222,8 @@ ChatCommand:radio-play: {
               name: commandTranslations.radioStationField,
               content: stationInfo.name,
             )
-            ..thumbnailUrl = stationInfo.image;
+            ..thumbnailUrl = stationInfo.image
+            ..url = stationInfo.url;
 
           final genre = stationInfo.genre;
           if (genre != null) {
@@ -255,28 +233,7 @@ ChatCommand:radio-play: {
             );
           }
 
-          embed.addField(
-            name: commandTranslations.computationalTimeField,
-            content: '${stopwatch.elapsedMilliseconds}ms',
-          );
-
-          final lyricsPages = stationInfo.lyricsPages(color: color);
-          if (lyricsPages == null || lyricsPages.isEmpty) {
-            final builder = ComponentMessageBuilder()..embeds = [embed];
-            linksResponse?.componentRows.forEach(builder.addComponentRow);
-            return await context.respond(builder);
-          }
-
-          final paginator = EmbedComponentPagination(
-            context.commands.interactions!,
-            [embed, ...lyricsPages],
-            user: context.user,
-          );
-
-          final messageBuilder = paginator.initMessageBuilder();
-          linksResponse?.componentRows.forEach(messageBuilder.addComponentRow);
-
-          await context.respond(messageBuilder);
+          await context.respond(MessageBuilder.embed(embed));
         } catch (e, stacktrace) {
           _logger.severe(
             'Failed to recognize radio',
@@ -316,8 +273,8 @@ ChatCommand:radio-play: {
 
         late GuildRadio? guildRadio;
         try {
-          guildRadio = await SongRecognitionService.instance
-              .currentRadio(context.guild!.id);
+          guildRadio =
+              await DatabaseService.instance.currentRadio(context.guild!.id);
         } on RadioNotPlayingException {
           await context.respond(
             MessageBuilder.embed(
diff --git a/lib/src/models/song_recognition/current_station_info.dart b/lib/src/models/song_recognition/current_station_info.dart
index f2d21ec..322e1f8 100644
--- a/lib/src/models/song_recognition/current_station_info.dart
+++ b/lib/src/models/song_recognition/current_station_info.dart
@@ -1,6 +1,6 @@
 import 'package:json_annotation/json_annotation.dart';
-import 'package:nyxx/nyxx.dart';
 import 'package:radio_horizon/radio_horizon.dart';
+import 'package:shazam_client/shazam_client.dart';
 
 part 'current_station_info.g.dart';
 
@@ -14,34 +14,26 @@ class CurrentStationInfo {
     this.title,
     this.image,
     this.url,
-  }) : _lyrics = null;
-
-  const CurrentStationInfo._lyrics({
-    this.description,
-    this.genre,
-    this.name,
-    this.title,
-    this.image,
-    this.url,
-    List<String>? lyrics,
-  })  : _lyrics = lyrics,
-        contentType = null;
+  });
 
   factory CurrentStationInfo.fromJson(Map<String, dynamic> json) =>
       _$CurrentStationInfoFromJson(json);
 
   factory CurrentStationInfo.fromShazamResult(
-    ShazamResult result,
-    GuildRadio radio,
-  ) =>
-      CurrentStationInfo._lyrics(
-        name: result.headline,
-        title: radio.station.name,
+    SongModel result, [
+    GuildRadio? guildRadio,
+  ]) =>
+      CurrentStationInfo(
+        title: '${result.title} - ${result.subtitle}',
         description: result.subtitle,
-        url: radio.station.urlResolved ?? radio.station.url,
-        image: result.share?.image ?? radio.station.favicon,
+        url: Uri.https(
+          'youtube.com',
+          '/results',
+          {'search_query': result.title},
+        ).toString(),
+        image: result.images?.coverart,
         genre: result.genres?.primary,
-        lyrics: result.lyrics,
+        name: guildRadio?.station.name,
       );
 
   CurrentStationInfo copyWith({
@@ -53,8 +45,7 @@ class CurrentStationInfo {
     String? url,
     List<String>? lyrics,
   }) =>
-      CurrentStationInfo._lyrics(
-        lyrics: lyrics ?? _lyrics,
+      CurrentStationInfo(
         description: description ?? this.description,
         genre: genre ?? this.genre,
         name: name ?? this.name,
@@ -83,46 +74,7 @@ class CurrentStationInfo {
   final String? url;
 
   final String? image;
-  final List<String>? _lyrics;
 
   bool get hasName => name != null && name!.isNotEmpty;
   bool get hasTitle => title != null && title!.isNotEmpty;
-
-  List<List<String>>? paragraphedLyrics(int paragraphsPerPage) {
-    if (_lyrics == null || _lyrics!.isEmpty) return null;
-    final lyrics = _lyrics ?? [];
-    final paragraphs = lyrics.join('\n').split('\n\n');
-
-    final m = (paragraphs.length / paragraphsPerPage).round();
-    final lists = List.generate(
-      3,
-      (i) => paragraphs.sublist(
-        m * i,
-        (i + 1) * m <= paragraphs.length ? (i + 1) * m : null,
-      ),
-    );
-
-    return lists;
-  }
-
-  List<EmbedBuilder>? lyricsPages({
-    required DiscordColor color,
-  }) {
-    final lyricsPages = <EmbedBuilder>[];
-    final llyrics = paragraphedLyrics(3);
-
-    if (llyrics == null) return null;
-
-    // add 3 paragraphs per page
-    for (var i = 0; i < llyrics.length; i++) {
-      final embed = EmbedBuilder()
-        ..color = color
-        ..title = title
-        ..description = llyrics[i].join('\n\n');
-
-      lyricsPages.add(embed);
-    }
-
-    return lyricsPages;
-  }
 }
diff --git a/lib/src/services/db.dart b/lib/src/services/db.dart
index 42d320f..77a0783 100644
--- a/lib/src/services/db.dart
+++ b/lib/src/services/db.dart
@@ -3,6 +3,7 @@ import 'dart:async';
 import 'package:logging/logging.dart';
 import 'package:mongo_dart/mongo_dart.dart';
 import 'package:nyxx/nyxx.dart';
+import 'package:radio_browser_api/radio_browser_api.dart';
 import 'package:radio_horizon/radio_horizon.dart';
 
 class DatabaseService {
@@ -63,6 +64,42 @@ class DatabaseService {
     }
   }
 
+  /// Gets the current radio playing by Guild.
+  ///
+  /// Throws [RadioNotPlayingException] if the Guild is not listening
+  /// to the radio
+  Future<GuildRadio> currentRadio(Snowflake guildId) async {
+    final currentlyPlaying = await getPlaying(guildId);
+    if (currentlyPlaying == null) {
+      throw const RadioNotPlayingException();
+    }
+
+    return currentlyPlaying;
+  }
+
+  /// Adds or not the current radio that the guild is playing
+  Future<void> setCurrentRadio(
+    Snowflake guildId,
+    Snowflake voiceChannelId,
+    Snowflake textChannelId,
+    Station station,
+  ) async {
+    final newRadio = GuildRadio(
+      guildId,
+      voiceChannelId: voiceChannelId,
+      station: station,
+      textChannelId: textChannelId,
+    );
+
+    await setPlaying(newRadio);
+  }
+
+  /// Deletes the radio from the [SongRecognitionService] radio. This is to let
+  /// the service know that the guild is no longer listening to the radio.
+  Future<void> deleteRadioFromList(Snowflake guildId) async {
+    await setNotPlaying(guildId);
+  }
+
   Future<GuildRadio?> getPlaying(Snowflake guildId) async {
     try {
       await _checkConnection();
diff --git a/lib/src/services/music.dart b/lib/src/services/music.dart
index 194d1b8..5cad787 100644
--- a/lib/src/services/music.dart
+++ b/lib/src/services/music.dart
@@ -155,7 +155,7 @@ class MusicService {
       guild.shard.changeVoiceState(guild.id, null);
 
       // delete the current radio station from the list, if it exists
-      await SongRecognitionService.instance.deleteRadioFromList(guild.id);
+      await DatabaseService.instance.deleteRadioFromList(guild.id);
 
       logger.info(
         'Disconnected from voice channel in guild ${guild.id} '
diff --git a/lib/src/services/song_recognition.dart b/lib/src/services/song_recognition.dart
index ecc2c87..1fbd0a5 100644
--- a/lib/src/services/song_recognition.dart
+++ b/lib/src/services/song_recognition.dart
@@ -9,79 +9,28 @@ import 'dart:convert';
 import 'dart:io';
 
 import 'package:http/http.dart' as http;
-import 'package:nyxx/nyxx.dart';
-import 'package:radio_browser_api/radio_browser_api.dart';
 import 'package:radio_horizon/radio_horizon.dart';
 import 'package:radio_horizon/src/models/song_recognition/current_station_info.dart';
+import 'package:shazam_client/shazam_client.dart';
 import 'package:uuid/uuid.dart';
 
 class SongRecognitionService {
-  SongRecognitionService._(
-    this.client,
-    this.databaseService,
-  ) : _httpClient = http.Client();
-
-  static void init(INyxxWebsocket client, DatabaseService databaseService) {
-    _instance = SongRecognitionService._(
-      client,
-      databaseService,
-    );
-  }
+  SongRecognitionService._privateConstructor()
+      : _shazamClient = ShazamClient.dockerized();
 
-  final DatabaseService databaseService;
-  final INyxxWebsocket client;
+  static SongRecognitionService get instance => _instance;
 
-  static SongRecognitionService get instance =>
-      _instance ??
-      (throw Exception('Song Recognition service must be initialised'));
-  static SongRecognitionService? _instance;
+  static final SongRecognitionService _instance =
+      SongRecognitionService._privateConstructor();
 
   Uuid get uuid => const Uuid();
 
-  http.Client get httpClient =>
-      _httpClient ??
-      (throw RadioCantCommunicateWithServer(
-        Exception('Http Client must be initialized'),
-      ));
-
-  // HttpClient used to get the sample
-  final http.Client? _httpClient;
-
-  /// Gets the current radio playing by Guild.
-  ///
-  /// Throws [RadioNotPlayingException] if the Guild is not listening
-  /// to the radio
-  Future<GuildRadio> currentRadio(Snowflake guildId) async {
-    final currentlyPlaying = await databaseService.getPlaying(guildId);
-    if (currentlyPlaying == null) {
-      throw const RadioNotPlayingException();
-    }
-
-    return currentlyPlaying;
-  }
+  ShazamClient get shazamClient => _shazamClient;
 
-  /// Adds or not the current radio that the guild is playing
-  Future<void> setCurrentRadio(
-    Snowflake guildId,
-    Snowflake voiceChannelId,
-    Snowflake textChannelId,
-    Station station,
-  ) async {
-    final newRadio = GuildRadio(
-      guildId,
-      voiceChannelId: voiceChannelId,
-      station: station,
-      textChannelId: textChannelId,
-    );
+  // ShazamClient used to get the sample
+  final ShazamClient _shazamClient;
 
-    await databaseService.setPlaying(newRadio);
-  }
-
-  /// Deletes the radio from the [SongRecognitionService] radio. This is to let
-  /// the service know that the guild is no longer listening to the radio.
-  Future<void> deleteRadioFromList(Snowflake guildId) async {
-    await databaseService.setNotPlaying(guildId);
-  }
+  final http.Client httpClient = http.Client();
 
   Future<CurrentStationInfo> getCurrentStationInfo(
     GuildRadio radio,
@@ -107,7 +56,7 @@ class SongRecognitionService {
   ///
   /// Receives a [url] to identify the radio and a [durationInSeconds] to
   /// grab that amount of time of the sample from the radio.
-  Future<ShazamResult> identify(
+  Future<SongModel> identify(
     String url,
     int? durationInSeconds,
   ) async {
@@ -116,41 +65,14 @@ class SongRecognitionService {
       durationInSeconds: durationInSeconds ?? 10,
     );
 
-    final sample = await songFile.readAsBytes();
-
     try {
-      final uri = Uri(
-        scheme: 'https',
-        host: 'shazam-song-recognizer.p.rapidapi.com',
-        path: 'recognize',
-      );
-
-      final request = http.MultipartRequest('POST', uri);
-
-      request.headers.addAll({
-        'X-RapidAPI-Key': rapidapiShazamSongRecognizerKey,
-        'X-RapidAPI-Host': 'shazam-song-recognizer.p.rapidapi.com',
-      });
-
-      request.files.add(
-        http.MultipartFile.fromBytes(
-          'upload_file',
-          sample,
-        ),
-      );
-
-      final streamedResponse = await request.send();
-      final response = await http.Response.fromStream(streamedResponse);
-      final result = ShazamSongRecognition.fromJson(
-        (jsonDecode(response.body) as Map).cast(),
-      );
-
-      final track = result.result;
+      final response = await shazamClient.recognizeSong(songFile);
+      final track = response;
 
       // Deletes the file after the recognition is done
       songFile.deleteSync();
 
-      return track ?? (throw const RadioCantIdentifySongException());
+      return track;
     } on RadioCantIdentifySongException {
       rethrow;
     } catch (e) {
diff --git a/pubspec.yaml b/pubspec.yaml
index 9482d1c..7ec07a2 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -16,21 +16,23 @@ dependencies:
   http: ">=0.13.0 <1.1.0"
   json_annotation: ^4.8.1
   logging: ^1.2.0
-  mongo_dart: ^0.9.3
+  mongo_dart: ^0.10.3
   nyxx: ^5.1.1
   nyxx_commands: ^5.0.2
   nyxx_interactions: ^4.6.0
   nyxx_lavalink: ^3.2.0
   nyxx_pagination: ^2.4.0
-  radio_browser_api: ^1.0.0
+  radio_browser_api: ^2.0.0+1
   retry: ^3.1.2
   sentry: ^7.10.1
   sentry_logging: ^7.10.1
+  shazam_client:
+    path: shazam_client
   shelf: ^1.4.1
   shelf_router: ^1.1.4
   slang: ^3.24.0
   time_ago_provider: ^4.2.0
-  uuid: ^3.0.7
+  uuid: ^4.0.0
 
 dev_dependencies:
   build_runner: ^2.4.6
diff --git a/sample.mp3 b/sample.mp3
index a1782e4..cd64610 100644
Binary files a/sample.mp3 and b/sample.mp3 differ
diff --git a/shazam_api/.dockerignore b/shazam_api/.dockerignore
new file mode 100644
index 0000000..785a7fe
--- /dev/null
+++ b/shazam_api/.dockerignore
@@ -0,0 +1,13 @@
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+.Python
+db.sqlite3
+.env
+venv/
+.git
+.dockerignore
+.gitignore
+Dockerfile
+*.md
diff --git a/shazam_api/Dockerfile b/shazam_api/Dockerfile
new file mode 100644
index 0000000..408b0ba
--- /dev/null
+++ b/shazam_api/Dockerfile
@@ -0,0 +1,26 @@
+# Use an official Python runtime as a parent image
+FROM python:3.12-slim
+
+# Set the working directory to /app
+WORKDIR /app
+
+# Install ffmpeg
+RUN apt-get update && \
+    apt-get install -y ffmpeg && \
+    rm -rf /var/lib/apt/lists/*
+
+# Copy the current directory contents into the container at /app
+COPY . /app
+
+# Install any needed packages specified in requirements.txt
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Make port 5000 available to the world outside this container
+EXPOSE 5000
+
+# Define environment variable
+ENV FLASK_APP=app.py
+ENV FLASK_RUN_HOST=0.0.0.0
+
+# Run app.py when the container launches
+CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"]
diff --git a/shazam_api/app.py b/shazam_api/app.py
new file mode 100644
index 0000000..63189e2
--- /dev/null
+++ b/shazam_api/app.py
@@ -0,0 +1,85 @@
+from flask import Flask, request, jsonify
+from shazamio import Shazam
+import asyncio
+from concurrent.futures import ThreadPoolExecutor
+
+app = Flask(__name__)
+executor = ThreadPoolExecutor()
+
+def run_async(func):
+    def wrapper(*args, **kwargs):
+        return asyncio.run(func(*args, **kwargs))
+    return wrapper
+
+@app.route('/recognize', methods=['GET'])
+def recognize_song():
+    if 'file' not in request.files:
+        return jsonify({'error': 'No file provided', 'statusCode': 500, 'path': request.path}), 400
+
+    file = request.files['file']
+    if file.filename == '':
+        return jsonify({'error': 'No file selected', 'statusCode': 400, 'path': request.path}), 400
+
+    if file and file.filename.lower().endswith('.mp3'):
+        # Read file data into memory
+        file_data = file.read()
+
+        # Run the asynchronous recognition
+        result = executor.submit(run_async(recognize_song_async), file_data).result()
+
+        if result and 'track' in result:
+            return jsonify(result['track'])
+        else:
+            return jsonify({'error': 'Song not recognized', 'statusCode': 404, 'path': request.path}), 404
+    else:
+        return jsonify({'error': 'Invalid file format. Please upload an MP3 file.', 'statusCode': 400, 'path': request.path}), 400
+
+@app.route('/search', methods=['GET'])
+def search_song():
+    if 'song' not in request.args:
+        return jsonify({'error': 'No song provided', 'statusCode': 400, 'path': request.path}), 400
+
+    song_name = request.args['song']
+    if song_name == '':
+        return jsonify({'error': 'No song selected', 'statusCode': 400, 'path': request.path}), 400
+
+    # Run the asynchronous search
+    result = executor.submit(run_async(search_song_async), song_name).result()
+    
+    if result and 'tracks' in result:
+        return jsonify(result['tracks'])
+    else:
+        return jsonify({'error': 'Song not found', 'statusCode': 404, 'path': request.path}), 404
+
+@app.route('/track', methods=['GET'])
+def track_data():
+    if 'id' not in request.args:
+        return jsonify({'error': 'No track ID provided', 'statusCode': 400, 'path': request.path}), 400
+
+    track_id = request.args['id']
+    if track_id == '':
+        return jsonify({'error': 'No track ID selected', 'statusCode': 400, 'path': request.path}), 400
+
+    # Run the asynchronous track data retrieval
+    result = executor.submit(run_async(track_data_async), track_id).result()
+    print(result)
+    
+    if result and 'title' in result:
+        return jsonify(result)
+    else:
+        return jsonify({'error': 'Track not found', 'statusCode': 404, 'path': request.path}), 404
+
+async def recognize_song_async(file_data):
+    shazam = Shazam()
+    return await shazam.recognize(file_data)
+
+async def search_song_async(song_name):
+    shazam = Shazam()
+    return await shazam.search_track(query=song_name, limit=5)
+
+async def track_data_async(track_id):
+    shazam = Shazam()
+    return await shazam.track_about(track_id=track_id)
+
+if __name__ == '__main__':
+    app.run(debug=False)
diff --git a/shazam_api/bin/Activate.ps1 b/shazam_api/bin/Activate.ps1
new file mode 100644
index 0000000..b49d77b
--- /dev/null
+++ b/shazam_api/bin/Activate.ps1
@@ -0,0 +1,247 @@
+<#
+.Synopsis
+Activate a Python virtual environment for the current PowerShell session.
+
+.Description
+Pushes the python executable for a virtual environment to the front of the
+$Env:PATH environment variable and sets the prompt to signify that you are
+in a Python virtual environment. Makes use of the command line switches as
+well as the `pyvenv.cfg` file values present in the virtual environment.
+
+.Parameter VenvDir
+Path to the directory that contains the virtual environment to activate. The
+default value for this is the parent of the directory that the Activate.ps1
+script is located within.
+
+.Parameter Prompt
+The prompt prefix to display when this virtual environment is activated. By
+default, this prompt is the name of the virtual environment folder (VenvDir)
+surrounded by parentheses and followed by a single space (ie. '(.venv) ').
+
+.Example
+Activate.ps1
+Activates the Python virtual environment that contains the Activate.ps1 script.
+
+.Example
+Activate.ps1 -Verbose
+Activates the Python virtual environment that contains the Activate.ps1 script,
+and shows extra information about the activation as it executes.
+
+.Example
+Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
+Activates the Python virtual environment located in the specified location.
+
+.Example
+Activate.ps1 -Prompt "MyPython"
+Activates the Python virtual environment that contains the Activate.ps1 script,
+and prefixes the current prompt with the specified string (surrounded in
+parentheses) while the virtual environment is active.
+
+.Notes
+On Windows, it may be required to enable this Activate.ps1 script by setting the
+execution policy for the user. You can do this by issuing the following PowerShell
+command:
+
+PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
+
+For more information on Execution Policies: 
+https://go.microsoft.com/fwlink/?LinkID=135170
+
+#>
+Param(
+    [Parameter(Mandatory = $false)]
+    [String]
+    $VenvDir,
+    [Parameter(Mandatory = $false)]
+    [String]
+    $Prompt
+)
+
+<# Function declarations --------------------------------------------------- #>
+
+<#
+.Synopsis
+Remove all shell session elements added by the Activate script, including the
+addition of the virtual environment's Python executable from the beginning of
+the PATH variable.
+
+.Parameter NonDestructive
+If present, do not remove this function from the global namespace for the
+session.
+
+#>
+function global:deactivate ([switch]$NonDestructive) {
+    # Revert to original values
+
+    # The prior prompt:
+    if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
+        Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
+        Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
+    }
+
+    # The prior PYTHONHOME:
+    if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
+        Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
+        Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
+    }
+
+    # The prior PATH:
+    if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
+        Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
+        Remove-Item -Path Env:_OLD_VIRTUAL_PATH
+    }
+
+    # Just remove the VIRTUAL_ENV altogether:
+    if (Test-Path -Path Env:VIRTUAL_ENV) {
+        Remove-Item -Path env:VIRTUAL_ENV
+    }
+
+    # Just remove VIRTUAL_ENV_PROMPT altogether.
+    if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
+        Remove-Item -Path env:VIRTUAL_ENV_PROMPT
+    }
+
+    # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
+    if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
+        Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
+    }
+
+    # Leave deactivate function in the global namespace if requested:
+    if (-not $NonDestructive) {
+        Remove-Item -Path function:deactivate
+    }
+}
+
+<#
+.Description
+Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
+given folder, and returns them in a map.
+
+For each line in the pyvenv.cfg file, if that line can be parsed into exactly
+two strings separated by `=` (with any amount of whitespace surrounding the =)
+then it is considered a `key = value` line. The left hand string is the key,
+the right hand is the value.
+
+If the value starts with a `'` or a `"` then the first and last character is
+stripped from the value before being captured.
+
+.Parameter ConfigDir
+Path to the directory that contains the `pyvenv.cfg` file.
+#>
+function Get-PyVenvConfig(
+    [String]
+    $ConfigDir
+) {
+    Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
+
+    # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
+    $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
+
+    # An empty map will be returned if no config file is found.
+    $pyvenvConfig = @{ }
+
+    if ($pyvenvConfigPath) {
+
+        Write-Verbose "File exists, parse `key = value` lines"
+        $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
+
+        $pyvenvConfigContent | ForEach-Object {
+            $keyval = $PSItem -split "\s*=\s*", 2
+            if ($keyval[0] -and $keyval[1]) {
+                $val = $keyval[1]
+
+                # Remove extraneous quotations around a string value.
+                if ("'""".Contains($val.Substring(0, 1))) {
+                    $val = $val.Substring(1, $val.Length - 2)
+                }
+
+                $pyvenvConfig[$keyval[0]] = $val
+                Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
+            }
+        }
+    }
+    return $pyvenvConfig
+}
+
+
+<# Begin Activate script --------------------------------------------------- #>
+
+# Determine the containing directory of this script
+$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
+$VenvExecDir = Get-Item -Path $VenvExecPath
+
+Write-Verbose "Activation script is located in path: '$VenvExecPath'"
+Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
+Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
+
+# Set values required in priority: CmdLine, ConfigFile, Default
+# First, get the location of the virtual environment, it might not be
+# VenvExecDir if specified on the command line.
+if ($VenvDir) {
+    Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
+}
+else {
+    Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
+    $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
+    Write-Verbose "VenvDir=$VenvDir"
+}
+
+# Next, read the `pyvenv.cfg` file to determine any required value such
+# as `prompt`.
+$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
+
+# Next, set the prompt from the command line, or the config file, or
+# just use the name of the virtual environment folder.
+if ($Prompt) {
+    Write-Verbose "Prompt specified as argument, using '$Prompt'"
+}
+else {
+    Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
+    if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
+        Write-Verbose "  Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
+        $Prompt = $pyvenvCfg['prompt'];
+    }
+    else {
+        Write-Verbose "  Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
+        Write-Verbose "  Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
+        $Prompt = Split-Path -Path $venvDir -Leaf
+    }
+}
+
+Write-Verbose "Prompt = '$Prompt'"
+Write-Verbose "VenvDir='$VenvDir'"
+
+# Deactivate any currently active virtual environment, but leave the
+# deactivate function in place.
+deactivate -nondestructive
+
+# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
+# that there is an activated venv.
+$env:VIRTUAL_ENV = $VenvDir
+
+if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
+
+    Write-Verbose "Setting prompt to '$Prompt'"
+
+    # Set the prompt to include the env name
+    # Make sure _OLD_VIRTUAL_PROMPT is global
+    function global:_OLD_VIRTUAL_PROMPT { "" }
+    Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
+    New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
+
+    function global:prompt {
+        Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
+        _OLD_VIRTUAL_PROMPT
+    }
+    $env:VIRTUAL_ENV_PROMPT = $Prompt
+}
+
+# Clear PYTHONHOME
+if (Test-Path -Path Env:PYTHONHOME) {
+    Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
+    Remove-Item -Path Env:PYTHONHOME
+}
+
+# Add the venv to the PATH
+Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
+$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
diff --git a/shazam_api/bin/activate b/shazam_api/bin/activate
new file mode 100644
index 0000000..0b95e10
--- /dev/null
+++ b/shazam_api/bin/activate
@@ -0,0 +1,70 @@
+# This file must be used with "source bin/activate" *from bash*
+# You cannot run it directly
+
+deactivate () {
+    # reset old environment variables
+    if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
+        PATH="${_OLD_VIRTUAL_PATH:-}"
+        export PATH
+        unset _OLD_VIRTUAL_PATH
+    fi
+    if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
+        PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
+        export PYTHONHOME
+        unset _OLD_VIRTUAL_PYTHONHOME
+    fi
+
+    # Call hash to forget past commands. Without forgetting
+    # past commands the $PATH changes we made may not be respected
+    hash -r 2> /dev/null
+
+    if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
+        PS1="${_OLD_VIRTUAL_PS1:-}"
+        export PS1
+        unset _OLD_VIRTUAL_PS1
+    fi
+
+    unset VIRTUAL_ENV
+    unset VIRTUAL_ENV_PROMPT
+    if [ ! "${1:-}" = "nondestructive" ] ; then
+    # Self destruct!
+        unset -f deactivate
+    fi
+}
+
+# unset irrelevant variables
+deactivate nondestructive
+
+# on Windows, a path can contain colons and backslashes and has to be converted:
+if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
+    # transform D:\path\to\venv to /d/path/to/venv on MSYS
+    # and to /cygdrive/d/path/to/venv on Cygwin
+    export VIRTUAL_ENV=$(cygpath "/Users/Tomas/Downloads/venv")
+else
+    # use the path as-is
+    export VIRTUAL_ENV="/Users/Tomas/Downloads/venv"
+fi
+
+_OLD_VIRTUAL_PATH="$PATH"
+PATH="$VIRTUAL_ENV/bin:$PATH"
+export PATH
+
+# unset PYTHONHOME if set
+# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
+# could use `if (set -u; : $PYTHONHOME) ;` in bash
+if [ -n "${PYTHONHOME:-}" ] ; then
+    _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
+    unset PYTHONHOME
+fi
+
+if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
+    _OLD_VIRTUAL_PS1="${PS1:-}"
+    PS1="(venv) ${PS1:-}"
+    export PS1
+    VIRTUAL_ENV_PROMPT="(venv) "
+    export VIRTUAL_ENV_PROMPT
+fi
+
+# Call hash to forget past commands. Without forgetting
+# past commands the $PATH changes we made may not be respected
+hash -r 2> /dev/null
diff --git a/shazam_api/bin/activate.csh b/shazam_api/bin/activate.csh
new file mode 100644
index 0000000..f3650b1
--- /dev/null
+++ b/shazam_api/bin/activate.csh
@@ -0,0 +1,27 @@
+# This file must be used with "source bin/activate.csh" *from csh*.
+# You cannot run it directly.
+
+# Created by Davide Di Blasi <davidedb@gmail.com>.
+# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
+
+alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
+
+# Unset irrelevant variables.
+deactivate nondestructive
+
+setenv VIRTUAL_ENV "/Users/Tomas/Downloads/venv"
+
+set _OLD_VIRTUAL_PATH="$PATH"
+setenv PATH "$VIRTUAL_ENV/bin:$PATH"
+
+
+set _OLD_VIRTUAL_PROMPT="$prompt"
+
+if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
+    set prompt = "(venv) $prompt"
+    setenv VIRTUAL_ENV_PROMPT "(venv) "
+endif
+
+alias pydoc python -m pydoc
+
+rehash
diff --git a/shazam_api/bin/activate.fish b/shazam_api/bin/activate.fish
new file mode 100644
index 0000000..65e3ba6
--- /dev/null
+++ b/shazam_api/bin/activate.fish
@@ -0,0 +1,69 @@
+# This file must be used with "source <venv>/bin/activate.fish" *from fish*
+# (https://fishshell.com/). You cannot run it directly.
+
+function deactivate  -d "Exit virtual environment and return to normal shell environment"
+    # reset old environment variables
+    if test -n "$_OLD_VIRTUAL_PATH"
+        set -gx PATH $_OLD_VIRTUAL_PATH
+        set -e _OLD_VIRTUAL_PATH
+    end
+    if test -n "$_OLD_VIRTUAL_PYTHONHOME"
+        set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
+        set -e _OLD_VIRTUAL_PYTHONHOME
+    end
+
+    if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
+        set -e _OLD_FISH_PROMPT_OVERRIDE
+        # prevents error when using nested fish instances (Issue #93858)
+        if functions -q _old_fish_prompt
+            functions -e fish_prompt
+            functions -c _old_fish_prompt fish_prompt
+            functions -e _old_fish_prompt
+        end
+    end
+
+    set -e VIRTUAL_ENV
+    set -e VIRTUAL_ENV_PROMPT
+    if test "$argv[1]" != "nondestructive"
+        # Self-destruct!
+        functions -e deactivate
+    end
+end
+
+# Unset irrelevant variables.
+deactivate nondestructive
+
+set -gx VIRTUAL_ENV "/Users/Tomas/Downloads/venv"
+
+set -gx _OLD_VIRTUAL_PATH $PATH
+set -gx PATH "$VIRTUAL_ENV/bin" $PATH
+
+# Unset PYTHONHOME if set.
+if set -q PYTHONHOME
+    set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
+    set -e PYTHONHOME
+end
+
+if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
+    # fish uses a function instead of an env var to generate the prompt.
+
+    # Save the current fish_prompt function as the function _old_fish_prompt.
+    functions -c fish_prompt _old_fish_prompt
+
+    # With the original prompt function renamed, we can override with our own.
+    function fish_prompt
+        # Save the return status of the last command.
+        set -l old_status $status
+
+        # Output the venv prompt; color taken from the blue of the Python logo.
+        printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal)
+
+        # Restore the return status of the previous command.
+        echo "exit $old_status" | .
+        # Output the original/"old" prompt.
+        _old_fish_prompt
+    end
+
+    set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
+    set -gx VIRTUAL_ENV_PROMPT "(venv) "
+end
diff --git a/shazam_api/bin/f2py b/shazam_api/bin/f2py
new file mode 100755
index 0000000..496ab76
--- /dev/null
+++ b/shazam_api/bin/f2py
@@ -0,0 +1,8 @@
+#!/Users/nazarenocavazzon/Documents/tests/venv/bin/python3.12
+# -*- coding: utf-8 -*-
+import re
+import sys
+from numpy.f2py.f2py2e import main
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())
diff --git a/shazam_api/bin/flask b/shazam_api/bin/flask
new file mode 100755
index 0000000..61d5b15
--- /dev/null
+++ b/shazam_api/bin/flask
@@ -0,0 +1,8 @@
+#!/Users/nazarenocavazzon/Documents/tests/venv/bin/python3.12
+# -*- coding: utf-8 -*-
+import re
+import sys
+from flask.cli import main
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())
diff --git a/shazam_api/bin/pip b/shazam_api/bin/pip
new file mode 100755
index 0000000..974a334
--- /dev/null
+++ b/shazam_api/bin/pip
@@ -0,0 +1,8 @@
+#!/Users/nazarenocavazzon/Documents/tests/venv/bin/python3.12
+# -*- coding: utf-8 -*-
+import re
+import sys
+from pip._internal.cli.main import main
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())
diff --git a/shazam_api/bin/pip3 b/shazam_api/bin/pip3
new file mode 100755
index 0000000..974a334
--- /dev/null
+++ b/shazam_api/bin/pip3
@@ -0,0 +1,8 @@
+#!/Users/nazarenocavazzon/Documents/tests/venv/bin/python3.12
+# -*- coding: utf-8 -*-
+import re
+import sys
+from pip._internal.cli.main import main
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())
diff --git a/shazam_api/bin/pip3.12 b/shazam_api/bin/pip3.12
new file mode 100755
index 0000000..974a334
--- /dev/null
+++ b/shazam_api/bin/pip3.12
@@ -0,0 +1,8 @@
+#!/Users/nazarenocavazzon/Documents/tests/venv/bin/python3.12
+# -*- coding: utf-8 -*-
+import re
+import sys
+from pip._internal.cli.main import main
+if __name__ == '__main__':
+    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+    sys.exit(main())
diff --git a/shazam_api/bin/python b/shazam_api/bin/python
new file mode 120000
index 0000000..11b9d88
--- /dev/null
+++ b/shazam_api/bin/python
@@ -0,0 +1 @@
+python3.12
\ No newline at end of file
diff --git a/shazam_api/bin/python3 b/shazam_api/bin/python3
new file mode 120000
index 0000000..11b9d88
--- /dev/null
+++ b/shazam_api/bin/python3
@@ -0,0 +1 @@
+python3.12
\ No newline at end of file
diff --git a/shazam_api/bin/python3.12 b/shazam_api/bin/python3.12
new file mode 120000
index 0000000..a3f0508
--- /dev/null
+++ b/shazam_api/bin/python3.12
@@ -0,0 +1 @@
+/opt/homebrew/opt/python@3.12/bin/python3.12
\ No newline at end of file
diff --git a/shazam_api/requirements.txt b/shazam_api/requirements.txt
new file mode 100644
index 0000000..e57a23f
--- /dev/null
+++ b/shazam_api/requirements.txt
@@ -0,0 +1,26 @@
+aiofiles==23.2.1
+aiohttp==3.9.5
+aiohttp-retry==2.8.3
+aiosignal==1.3.1
+anyio==4.3.0
+attrs==23.2.0
+blinker==1.8.2
+click==8.1.7
+dataclass-factory==2.16
+Flask==3.0.3
+frozenlist==1.4.1
+idna==3.7
+itsdangerous==2.2.0
+Jinja2==3.1.4
+MarkupSafe==2.1.5
+multidict==6.0.5
+numpy==1.26.4
+pydantic==1.10.17
+pydub==0.25.1
+shazamio==0.6.0
+shazamio_core==1.0.7
+sniffio==1.3.1
+typing_extensions==4.12.2
+Werkzeug==3.0.3
+yarl==1.9.4
+gunicorn
\ No newline at end of file
diff --git a/shazam_client/.gitignore b/shazam_client/.gitignore
new file mode 100644
index 0000000..526da15
--- /dev/null
+++ b/shazam_client/.gitignore
@@ -0,0 +1,7 @@
+# See https://www.dartlang.org/guides/libraries/private-files
+
+# Files and directories created by pub
+.dart_tool/
+.packages
+build/
+pubspec.lock
\ No newline at end of file
diff --git a/shazam_client/README.md b/shazam_client/README.md
new file mode 100644
index 0000000..7ab0659
--- /dev/null
+++ b/shazam_client/README.md
@@ -0,0 +1,62 @@
+# Shazam Client
+
+[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
+[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason)
+[![License: MIT][license_badge]][license_link]
+
+A Very Good Project created by Very Good CLI.
+
+## Installation ๐Ÿ’ป
+
+**โ— In order to start using Shazam Client you must have the [Dart SDK][dart_install_link] installed on your machine.**
+
+Install via `dart pub add`:
+
+```sh
+dart pub add shazam_client
+```
+
+---
+
+## Continuous Integration ๐Ÿค–
+
+Shazam Client comes with a built-in [GitHub Actions workflow][github_actions_link] powered by [Very Good Workflows][very_good_workflows_link] but you can also add your preferred CI/CD solution.
+
+Out of the box, on each pull request and push, the CI `formats`, `lints`, and `tests` the code. This ensures the code remains consistent and behaves correctly as you add functionality or make changes. The project uses [Very Good Analysis][very_good_analysis_link] for a strict set of analysis options used by our team. Code coverage is enforced using the [Very Good Workflows][very_good_coverage_link].
+
+---
+
+## Running Tests ๐Ÿงช
+
+To run all unit tests:
+
+```sh
+dart pub global activate coverage 1.2.0
+dart test --coverage=coverage
+dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info
+```
+
+To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov).
+
+```sh
+# Generate Coverage Report
+genhtml coverage/lcov.info -o coverage/
+
+# Open Coverage Report
+open coverage/index.html
+```
+
+[dart_install_link]: https://dart.dev/get-dart
+[github_actions_link]: https://docs.github.com/en/actions/learn-github-actions
+[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
+[license_link]: https://opensource.org/licenses/MIT
+[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only
+[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only
+[mason_link]: https://github.com/felangel/mason
+[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
+[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
+[very_good_coverage_link]: https://github.com/marketplace/actions/very-good-coverage
+[very_good_ventures_link]: https://verygood.ventures
+[very_good_ventures_link_light]: https://verygood.ventures#gh-light-mode-only
+[very_good_ventures_link_dark]: https://verygood.ventures#gh-dark-mode-only
+[very_good_workflows_link]: https://github.com/VeryGoodOpenSource/very_good_workflows
diff --git a/shazam_client/analysis_options.yaml b/shazam_client/analysis_options.yaml
new file mode 100644
index 0000000..30f632c
--- /dev/null
+++ b/shazam_client/analysis_options.yaml
@@ -0,0 +1,9 @@
+include: package:very_good_analysis/analysis_options.6.0.0.yaml
+
+linter:
+  rules:
+    public_member_api_docs: false
+
+analyzer:
+  exclude:
+    - "**.g.dart"
diff --git a/shazam_client/coverage_badge.svg b/shazam_client/coverage_badge.svg
new file mode 100644
index 0000000..499e98c
--- /dev/null
+++ b/shazam_client/coverage_badge.svg
@@ -0,0 +1,20 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="102" height="20">
+    <linearGradient id="b" x2="0" y2="100%">
+        <stop offset="0" stop-color="#bbb" stop-opacity=".1" />
+        <stop offset="1" stop-opacity=".1" />
+    </linearGradient>
+    <clipPath id="a">
+        <rect width="102" height="20" rx="3" fill="#fff" />
+    </clipPath>
+    <g clip-path="url(#a)">
+        <path fill="#555" d="M0 0h59v20H0z" />
+        <path fill="#44cc11" d="M59 0h43v20H59z" />
+        <path fill="url(#b)" d="M0 0h102v20H0z" />
+    </g>
+    <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
+        <text x="305" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">coverage</text>
+        <text x="305" y="140" transform="scale(.1)" textLength="490">coverage</text>
+        <text x="795" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">100%</text>
+        <text x="795" y="140" transform="scale(.1)" textLength="330">100%</text>
+    </g>
+</svg>
diff --git a/shazam_client/lib/shazam_client.dart b/shazam_client/lib/shazam_client.dart
new file mode 100644
index 0000000..b09f0a5
--- /dev/null
+++ b/shazam_client/lib/shazam_client.dart
@@ -0,0 +1,6 @@
+/// A Very Good Project created by Very Good CLI.
+library;
+
+export 'src/exceptions.dart';
+export 'src/models/models.dart';
+export 'src/shazam_client.dart';
diff --git a/shazam_client/lib/src/exceptions.dart b/shazam_client/lib/src/exceptions.dart
new file mode 100644
index 0000000..7e9c78a
--- /dev/null
+++ b/shazam_client/lib/src/exceptions.dart
@@ -0,0 +1,58 @@
+/// Thrown if an exception occurs while making an http request.
+class HttpException implements Exception {}
+
+/// {@template http_request_failure}
+/// Thrown if an http request returns a non-200 status code.
+/// {@endtemplate}
+class HttpRequestFailure implements Exception {
+  /// {@macro http_request_failure}
+  const HttpRequestFailure(this.statusCode, this.error);
+
+  /// The status code of the response.
+  final int statusCode;
+
+  /// The error message of the response.
+  final String error;
+
+  @override
+  String toString() =>
+      'HttpRequestFailure(statusCode: $statusCode, error: $error)';
+}
+
+/// Thrown when an error occurs while decoding the response body.
+class JsonDecodeException implements Exception {
+  /// Thrown when an error occurs while decoding the response body.
+  const JsonDecodeException();
+}
+
+/// Thrown when an error occurs while decoding the response body.
+class SpecifiedTypeNotMatchedException implements Exception {
+  /// Thrown when an error occurs while decoding the response body.
+  const SpecifiedTypeNotMatchedException();
+}
+
+/// Thrown when an unknown error occurs.
+class UnknownException implements Exception {
+  /// Thrown when an unknown error occurs.
+  const UnknownException();
+}
+
+/// This exception is thrown if the server sends a request with an unexpected
+/// status code or missing/invalid headers.
+class ProtocolException implements Exception {
+  /// Create a new ProtocolException.
+  ProtocolException(
+    this.message, [
+    this.code,
+  ]);
+
+  /// Message from the exception
+  final String message;
+
+  /// Code from the exception.
+  final int? code;
+
+  /// Returns a string representation of this exception.
+  @override
+  String toString() => 'ProtocolException: ($code) $message';
+}
diff --git a/shazam_client/lib/src/models/action.dart b/shazam_client/lib/src/models/action.dart
new file mode 100644
index 0000000..e7d7817
--- /dev/null
+++ b/shazam_client/lib/src/models/action.dart
@@ -0,0 +1,50 @@
+import 'dart:convert';
+
+class Action {
+  Action({
+    this.id,
+    this.name,
+    this.type,
+    this.uri,
+  });
+
+  factory Action.fromRawJson(String str) =>
+      Action.fromJson(json.decode(str) as Map);
+
+  factory Action.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Action(
+      id: json['id'] as String?,
+      name: json['name'] as String?,
+      type: json['type'] as String?,
+      uri: json['uri'] as String?,
+    );
+  }
+  final String? id;
+  final String? name;
+  final String? type;
+  final String? uri;
+
+  Action copyWith({
+    String? id,
+    String? name,
+    String? type,
+    String? uri,
+  }) =>
+      Action(
+        id: id ?? this.id,
+        name: name ?? this.name,
+        type: type ?? this.type,
+        uri: uri ?? this.uri,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'id': id,
+        'name': name,
+        'type': type,
+        'uri': uri,
+      };
+}
diff --git a/shazam_client/lib/src/models/artist_model.dart b/shazam_client/lib/src/models/artist_model.dart
new file mode 100644
index 0000000..34a0dec
--- /dev/null
+++ b/shazam_client/lib/src/models/artist_model.dart
@@ -0,0 +1,38 @@
+import 'dart:convert';
+
+class Artist {
+  Artist({
+    this.adamid,
+    this.id,
+  });
+
+  factory Artist.fromRawJson(String str) =>
+      Artist.fromJson(json.decode(str) as Map);
+
+  factory Artist.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Artist(
+      adamid: json['adamid'] as String?,
+      id: json['id'] as String?,
+    );
+  }
+  final String? adamid;
+  final String? id;
+
+  Artist copyWith({
+    String? adamid,
+    String? id,
+  }) =>
+      Artist(
+        adamid: adamid ?? this.adamid,
+        id: id ?? this.id,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'adamid': adamid,
+        'id': id,
+      };
+}
diff --git a/shazam_client/lib/src/models/beacondata.dart b/shazam_client/lib/src/models/beacondata.dart
new file mode 100644
index 0000000..692644e
--- /dev/null
+++ b/shazam_client/lib/src/models/beacondata.dart
@@ -0,0 +1,38 @@
+import 'dart:convert';
+
+class Beacondata {
+  Beacondata({
+    this.providername,
+    this.type,
+  });
+
+  factory Beacondata.fromRawJson(String str) =>
+      Beacondata.fromJson(json.decode(str) as Map);
+
+  factory Beacondata.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Beacondata(
+      providername: json['providername'] as String?,
+      type: json['type'] as String?,
+    );
+  }
+  final String? providername;
+  final String? type;
+
+  Beacondata copyWith({
+    String? providername,
+    String? type,
+  }) =>
+      Beacondata(
+        providername: providername ?? this.providername,
+        type: type ?? this.type,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'providername': providername,
+        'type': type,
+      };
+}
diff --git a/shazam_client/lib/src/models/exception_response_model.dart b/shazam_client/lib/src/models/exception_response_model.dart
new file mode 100644
index 0000000..7a78c65
--- /dev/null
+++ b/shazam_client/lib/src/models/exception_response_model.dart
@@ -0,0 +1,54 @@
+import 'dart:convert';
+
+import 'package:equatable/equatable.dart';
+import 'package:http/http.dart' as http;
+
+/// {@template exception_response}
+/// A class that represents the error response from the backend.
+/// {@endtemplate}
+class ExceptionResponse with EquatableMixin implements Exception {
+  /// {@macro exception_response}
+  const ExceptionResponse({
+    this.error,
+    this.statusCode,
+    this.path,
+  });
+
+  /// Creates an [ExceptionResponse] from a JSON object.
+  factory ExceptionResponse.fromJson(Map<String, dynamic> json) {
+    return ExceptionResponse(
+      error: json['error'] as String?,
+      statusCode: json['statusCode'] as int? ?? 500,
+      path: json['path'] as String?,
+    );
+  }
+
+  /// Checks if the response is a JSON error response.
+  static bool matches(http.Response response) {
+    if (response.statusCode != 200) {
+      try {
+        jsonDecode(response.body);
+        return true;
+      } catch (_) {
+        // If decoding fails, it might not be a JSON response.
+        return false;
+      }
+    }
+    return false;
+  }
+
+  /// Merges the [ExceptionResponse] with the [http.Response].
+  final String? error;
+
+  /// The status code of the response.
+  final int? statusCode;
+
+  /// The path of the request that caused the error.
+  final String? path;
+
+  @override
+  String toString() =>
+      'ExceptionResponse(error: $error, statusCode: $statusCode, path: $path)';
+  @override
+  List<Object?> get props => [error, statusCode, path];
+}
diff --git a/shazam_client/lib/src/models/genres.dart b/shazam_client/lib/src/models/genres.dart
new file mode 100644
index 0000000..1df8570
--- /dev/null
+++ b/shazam_client/lib/src/models/genres.dart
@@ -0,0 +1,38 @@
+import 'dart:convert';
+
+class Genres {
+  Genres({
+    this.primary,
+    this.secondary,
+  });
+
+  factory Genres.fromRawJson(String str) =>
+      Genres.fromJson(json.decode(str) as Map);
+
+  factory Genres.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Genres(
+      primary: json['primary'] as String?,
+      secondary: json['secondary'] as String?,
+    );
+  }
+  final String? primary;
+  final String? secondary;
+
+  Genres copyWith({
+    String? primary,
+    String? secondary,
+  }) =>
+      Genres(
+        primary: primary ?? this.primary,
+        secondary: secondary ?? this.secondary,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'primary': primary,
+        'secondary': secondary,
+      };
+}
diff --git a/shazam_client/lib/src/models/hub.dart b/shazam_client/lib/src/models/hub.dart
new file mode 100644
index 0000000..7f96f9b
--- /dev/null
+++ b/shazam_client/lib/src/models/hub.dart
@@ -0,0 +1,94 @@
+
+import 'dart:convert';
+
+import 'package:shazam_client/shazam_client.dart';
+
+class Hub {
+  Hub({
+    this.actions,
+    this.displayname,
+    this.explicit,
+    this.image,
+    this.options,
+    this.providers,
+    this.type,
+  });
+
+  factory Hub.fromRawJson(String str) => Hub.fromJson(json.decode(str) as Map);
+
+  factory Hub.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Hub(
+      actions: json['actions'] == null
+          ? null
+          : List<Action>.from(
+              (json['actions'] as List)
+                  .cast<Map<dynamic, dynamic>>()
+                  .map(Action.fromJson),
+            ),
+      displayname: json['displayname'] as String?,
+      explicit: json['explicit'] as bool?,
+      image: json['image'] as String?,
+      options: json['options'] == null
+          ? null
+          : List<Option>.from(
+              (json['options'] as List)
+                  .cast<Map<dynamic, dynamic>>()
+                  .map(Option.fromJson),
+            ),
+      providers: json['providers'] == null
+          ? null
+          : List<Provider>.from(
+              (json['providers'] as List)
+                  .cast<Map<dynamic, dynamic>>()
+                  .map(Provider.fromJson),
+            ),
+      type: json['type'] as String?,
+    );
+  }
+  final List<Action>? actions;
+  final String? displayname;
+  final bool? explicit;
+  final String? image;
+  final List<Option>? options;
+  final List<Provider>? providers;
+  final String? type;
+
+  Hub copyWith({
+    List<Action>? actions,
+    String? displayname,
+    bool? explicit,
+    String? image,
+    List<Option>? options,
+    List<Provider>? providers,
+    String? type,
+  }) =>
+      Hub(
+        actions: actions ?? this.actions,
+        displayname: displayname ?? this.displayname,
+        explicit: explicit ?? this.explicit,
+        image: image ?? this.image,
+        options: options ?? this.options,
+        providers: providers ?? this.providers,
+        type: type ?? this.type,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'actions': actions == null
+            ? null
+            : List<dynamic>.from(actions!.map((x) => x.toJson())),
+        'displayname': displayname,
+        'explicit': explicit,
+        'image': image,
+        'options': options == null
+            ? null
+            : List<dynamic>.from(options!.map((x) => x.toJson())),
+        'providers': providers == null
+            ? null
+            : List<dynamic>.from(providers!.map((x) => x.toJson())),
+        'type': type,
+      };
+}
diff --git a/shazam_client/lib/src/models/metadatum.dart b/shazam_client/lib/src/models/metadatum.dart
new file mode 100644
index 0000000..1ad7e85
--- /dev/null
+++ b/shazam_client/lib/src/models/metadatum.dart
@@ -0,0 +1,38 @@
+import 'dart:convert';
+
+class Metadatum {
+  Metadatum({
+    this.text,
+    this.title,
+  });
+
+  factory Metadatum.fromRawJson(String str) =>
+      Metadatum.fromJson(json.decode(str) as Map);
+
+  factory Metadatum.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Metadatum(
+      text: json['text'] as String?,
+      title: json['title'] as String?,
+    );
+  }
+  final String? text;
+  final String? title;
+
+  Metadatum copyWith({
+    String? text,
+    String? title,
+  }) =>
+      Metadatum(
+        text: text ?? this.text,
+        title: title ?? this.title,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'text': text,
+        'title': title,
+      };
+}
diff --git a/shazam_client/lib/src/models/metapage.dart b/shazam_client/lib/src/models/metapage.dart
new file mode 100644
index 0000000..e778528
--- /dev/null
+++ b/shazam_client/lib/src/models/metapage.dart
@@ -0,0 +1,38 @@
+import 'dart:convert';
+
+class Metapage {
+  Metapage({
+    this.caption,
+    this.image,
+  });
+
+  factory Metapage.fromRawJson(String str) =>
+      Metapage.fromJson(json.decode(str) as Map);
+
+  factory Metapage.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Metapage(
+      caption: json['caption'] as String?,
+      image: json['image'] as String?,
+    );
+  }
+  final String? caption;
+  final String? image;
+
+  Metapage copyWith({
+    String? caption,
+    String? image,
+  }) =>
+      Metapage(
+        caption: caption ?? this.caption,
+        image: image ?? this.image,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'caption': caption,
+        'image': image,
+      };
+}
diff --git a/shazam_client/lib/src/models/models.dart b/shazam_client/lib/src/models/models.dart
new file mode 100644
index 0000000..900c7dc
--- /dev/null
+++ b/shazam_client/lib/src/models/models.dart
@@ -0,0 +1,23 @@
+export 'action.dart';
+export 'artist_model.dart';
+export 'beacondata.dart';
+export 'exception_response_model.dart';
+export 'genres.dart';
+export 'hub.dart';
+export 'metadatum.dart';
+export 'metapage.dart';
+export 'models.dart';
+export 'option.dart';
+export 'provider.dart';
+export 'provider_images.dart';
+export 'section.dart';
+export 'share.dart';
+export 'song_model.dart';
+export 'song_model_images.dart';
+export 'urlparams.dart';
+
+/// The JSON serializable model for the API response.
+typedef JSON = Map<String, dynamic>;
+
+/// When de API response is a List of [JSON]
+typedef JSONLIST = List<JSON>;
diff --git a/shazam_client/lib/src/models/option.dart b/shazam_client/lib/src/models/option.dart
new file mode 100644
index 0000000..7b97fda
--- /dev/null
+++ b/shazam_client/lib/src/models/option.dart
@@ -0,0 +1,92 @@
+import 'dart:convert';
+
+import 'package:shazam_client/shazam_client.dart';
+
+class Option {
+  Option({
+    this.actions,
+    this.beacondata,
+    this.caption,
+    this.colouroverflowimage,
+    this.image,
+    this.listcaption,
+    this.overflowimage,
+    this.providername,
+    this.type,
+  });
+
+  factory Option.fromRawJson(String str) =>
+      Option.fromJson(json.decode(str) as Map);
+
+  factory Option.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Option(
+      actions: json['actions'] == null
+          ? null
+          : List<Action>.from(
+              (json['actions'] as List)
+                  .cast<Map<dynamic, dynamic>>()
+                  .map(Action.fromJson),
+            ),
+      beacondata: json['beacondata'] == null
+          ? null
+          : Beacondata.fromJson(json['beacondata'] as Map),
+      caption: json['caption'] as String?,
+      colouroverflowimage: json['colouroverflowimage'] as bool?,
+      image: json['image'] as String?,
+      listcaption: json['listcaption'] as String?,
+      overflowimage: json['overflowimage'] as String?,
+      providername: json['providername'] as String?,
+      type: json['type'] as String?,
+    );
+  }
+  final List<Action>? actions;
+  final Beacondata? beacondata;
+  final String? caption;
+  final bool? colouroverflowimage;
+  final String? image;
+  final String? listcaption;
+  final String? overflowimage;
+  final String? providername;
+  final String? type;
+
+  Option copyWith({
+    List<Action>? actions,
+    Beacondata? beacondata,
+    String? caption,
+    bool? colouroverflowimage,
+    String? image,
+    String? listcaption,
+    String? overflowimage,
+    String? providername,
+    String? type,
+  }) =>
+      Option(
+        actions: actions ?? this.actions,
+        beacondata: beacondata ?? this.beacondata,
+        caption: caption ?? this.caption,
+        colouroverflowimage: colouroverflowimage ?? this.colouroverflowimage,
+        image: image ?? this.image,
+        listcaption: listcaption ?? this.listcaption,
+        overflowimage: overflowimage ?? this.overflowimage,
+        providername: providername ?? this.providername,
+        type: type ?? this.type,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'actions': actions == null
+            ? null
+            : List<dynamic>.from(actions!.map((x) => x.toJson())),
+        'beacondata': beacondata?.toJson(),
+        'caption': caption,
+        'colouroverflowimage': colouroverflowimage,
+        'image': image,
+        'listcaption': listcaption,
+        'overflowimage': overflowimage,
+        'providername': providername,
+        'type': type,
+      };
+}
diff --git a/shazam_client/lib/src/models/provider.dart b/shazam_client/lib/src/models/provider.dart
new file mode 100644
index 0000000..06063c9
--- /dev/null
+++ b/shazam_client/lib/src/models/provider.dart
@@ -0,0 +1,62 @@
+import 'dart:convert';
+
+import 'package:shazam_client/shazam_client.dart';
+
+class Provider {
+  Provider({
+    this.actions,
+    this.caption,
+    this.images,
+    this.type,
+  });
+
+  factory Provider.fromRawJson(String str) =>
+      Provider.fromJson(json.decode(str) as Map);
+
+  factory Provider.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Provider(
+      actions: json['actions'] == null
+          ? null
+          : List<Action>.from(
+              (json['actions'] as List)
+                  .cast<Map<dynamic, dynamic>>()
+                  .map(Action.fromJson),
+            ),
+      caption: json['caption'] as String?,
+      images: json['images'] == null
+          ? null
+          : ProviderImages.fromJson(json['images'] as Map),
+      type: json['type'] as String?,
+    );
+  }
+  final List<Action>? actions;
+  final String? caption;
+  final ProviderImages? images;
+  final String? type;
+
+  Provider copyWith({
+    List<Action>? actions,
+    String? caption,
+    ProviderImages? images,
+    String? type,
+  }) =>
+      Provider(
+        actions: actions ?? this.actions,
+        caption: caption ?? this.caption,
+        images: images ?? this.images,
+        type: type ?? this.type,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'actions': actions == null
+            ? null
+            : List<dynamic>.from(actions!.map((x) => x.toJson())),
+        'caption': caption,
+        'images': images?.toJson(),
+        'type': type,
+      };
+}
diff --git a/shazam_client/lib/src/models/provider_images.dart b/shazam_client/lib/src/models/provider_images.dart
new file mode 100644
index 0000000..ba9c35d
--- /dev/null
+++ b/shazam_client/lib/src/models/provider_images.dart
@@ -0,0 +1,38 @@
+import 'dart:convert';
+
+class ProviderImages {
+  ProviderImages({
+    this.imagesDefault,
+    this.overflow,
+  });
+
+  factory ProviderImages.fromRawJson(String str) =>
+      ProviderImages.fromJson(json.decode(str) as Map);
+
+  factory ProviderImages.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return ProviderImages(
+      imagesDefault: json['default'] as String?,
+      overflow: json['overflow'] as String?,
+    );
+  }
+  final String? imagesDefault;
+  final String? overflow;
+
+  ProviderImages copyWith({
+    String? imagesDefault,
+    String? overflow,
+  }) =>
+      ProviderImages(
+        imagesDefault: imagesDefault ?? this.imagesDefault,
+        overflow: overflow ?? this.overflow,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'default': imagesDefault,
+        'overflow': overflow,
+      };
+}
diff --git a/shazam_client/lib/src/models/section.dart b/shazam_client/lib/src/models/section.dart
new file mode 100644
index 0000000..ed2be20
--- /dev/null
+++ b/shazam_client/lib/src/models/section.dart
@@ -0,0 +1,74 @@
+import 'dart:convert';
+
+import 'package:shazam_client/shazam_client.dart';
+
+class Section {
+  Section({
+    this.metadata,
+    this.metapages,
+    this.tabname,
+    this.type,
+    this.url,
+  });
+
+  factory Section.fromRawJson(String str) =>
+      Section.fromJson(json.decode(str) as Map);
+
+  factory Section.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Section(
+      metadata: json['metadata'] == null
+          ? null
+          : List<Metadatum>.from(
+              (json['metadata'] as List)
+                  .cast<Map<dynamic, dynamic>>()
+                  .map(Metadatum.fromJson),
+            ),
+      metapages: json['metapages'] == null
+          ? null
+          : List<Metapage>.from(
+              (json['metapages'] as List)
+                  .cast<Map<dynamic, dynamic>>()
+                  .map(Metapage.fromJson),
+            ),
+      tabname: json['tabname'] as String?,
+      type: json['type'] as String?,
+      url: json['url'] as String?,
+    );
+  }
+  final List<Metadatum>? metadata;
+  final List<Metapage>? metapages;
+  final String? tabname;
+  final String? type;
+  final String? url;
+
+  Section copyWith({
+    List<Metadatum>? metadata,
+    List<Metapage>? metapages,
+    String? tabname,
+    String? type,
+    String? url,
+  }) =>
+      Section(
+        metadata: metadata ?? this.metadata,
+        metapages: metapages ?? this.metapages,
+        tabname: tabname ?? this.tabname,
+        type: type ?? this.type,
+        url: url ?? this.url,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'metadata': metadata == null
+            ? null
+            : List<dynamic>.from(metadata!.map((x) => x.toJson())),
+        'metapages': metapages == null
+            ? null
+            : List<dynamic>.from(metapages!.map((x) => x.toJson())),
+        'tabname': tabname,
+        'type': type,
+        'url': url,
+      };
+}
diff --git a/shazam_client/lib/src/models/share.dart b/shazam_client/lib/src/models/share.dart
new file mode 100644
index 0000000..34be4c7
--- /dev/null
+++ b/shazam_client/lib/src/models/share.dart
@@ -0,0 +1,75 @@
+import 'dart:convert';
+
+class Share {
+  Share({
+    this.avatar,
+    this.href,
+    this.html,
+    this.image,
+    this.snapchat,
+    this.subject,
+    this.text,
+    this.twitter,
+  });
+
+  factory Share.fromRawJson(String str) =>
+      Share.fromJson(json.decode(str) as Map);
+
+  factory Share.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Share(
+      avatar: json['avatar'] as String?,
+      href: json['href'] as String?,
+      html: json['html'] as String?,
+      image: json['image'] as String?,
+      snapchat: json['snapchat'] as String?,
+      subject: json['subject'] as String?,
+      text: json['text'] as String?,
+      twitter: json['twitter'] as String?,
+    );
+  }
+
+  final String? avatar;
+  final String? href;
+  final String? html;
+  final String? image;
+  final String? snapchat;
+  final String? subject;
+  final String? text;
+  final String? twitter;
+
+  Share copyWith({
+    String? avatar,
+    String? href,
+    String? html,
+    String? image,
+    String? snapchat,
+    String? subject,
+    String? text,
+    String? twitter,
+  }) =>
+      Share(
+        avatar: avatar ?? this.avatar,
+        href: href ?? this.href,
+        html: html ?? this.html,
+        image: image ?? this.image,
+        snapchat: snapchat ?? this.snapchat,
+        subject: subject ?? this.subject,
+        text: text ?? this.text,
+        twitter: twitter ?? this.twitter,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'avatar': avatar,
+        'href': href,
+        'html': html,
+        'image': image,
+        'snapchat': snapchat,
+        'subject': subject,
+        'text': text,
+        'twitter': twitter,
+      };
+}
diff --git a/shazam_client/lib/src/models/song_model.dart b/shazam_client/lib/src/models/song_model.dart
new file mode 100644
index 0000000..2b57d9a
--- /dev/null
+++ b/shazam_client/lib/src/models/song_model.dart
@@ -0,0 +1,148 @@
+import 'dart:convert';
+
+import 'package:shazam_client/shazam_client.dart';
+
+class SongModel {
+  SongModel({
+    this.albumadamid,
+    this.artists,
+    this.genres,
+    this.hub,
+    this.images,
+    this.isrc,
+    this.key,
+    this.layout,
+    this.relatedtracksurl,
+    this.sections,
+    this.share,
+    this.subtitle,
+    this.title,
+    this.type,
+    this.url,
+    this.urlparams,
+  });
+
+  factory SongModel.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return SongModel(
+      albumadamid: json['albumadamid'] as String?,
+      artists: json['artists'] == null
+          ? null
+          : List<Artist>.from(
+              (json['artists'] as List)
+                  .cast<Map<dynamic, dynamic>>()
+                  .map(Artist.fromJson),
+            ),
+      genres: json['genres'] == null
+          ? null
+          : Genres.fromJson(json['genres'] as Map),
+      hub: json['hub'] == null ? null : Hub.fromJson(json['hub'] as Map),
+      images: json['images'] == null
+          ? null
+          : SongModelImages.fromJson(json['images'] as Map),
+      isrc: json['isrc'] as String?,
+      key: json['key'] as String?,
+      layout: json['layout'] as String?,
+      relatedtracksurl: json['relatedtracksurl'] as String?,
+      sections: json['sections'] == null
+          ? null
+          : List<Section>.from(
+              (json['sections'] as List)
+                  .cast<Map<dynamic, dynamic>>()
+                  .map(Section.fromJson),
+            ),
+      share:
+          json['share'] == null ? null : Share.fromJson(json['share'] as Map),
+      subtitle: json['subtitle'] as String?,
+      title: json['title'] as String?,
+      type: json['type'] as String?,
+      url: json['url'] as String?,
+      urlparams: json['urlparams'] == null
+          ? null
+          : Urlparams.fromJson(json['urlparams'] as Map),
+    );
+  }
+
+  factory SongModel.fromRawJson(String str) =>
+      SongModel.fromJson(json.decode(str) as Map);
+
+  final String? albumadamid;
+  final List<Artist>? artists;
+  final Genres? genres;
+  final Hub? hub;
+  final SongModelImages? images;
+  final String? isrc;
+  final String? key;
+  final String? layout;
+  final String? relatedtracksurl;
+  final List<Section>? sections;
+  final Share? share;
+  final String? subtitle;
+  final String? title;
+  final String? type;
+  final String? url;
+  final Urlparams? urlparams;
+
+  SongModel copyWith({
+    String? albumadamid,
+    List<Artist>? artists,
+    Genres? genres,
+    Hub? hub,
+    SongModelImages? images,
+    String? isrc,
+    String? key,
+    String? layout,
+    String? relatedtracksurl,
+    List<Section>? sections,
+    Share? share,
+    String? subtitle,
+    String? title,
+    String? type,
+    String? url,
+    Urlparams? urlparams,
+  }) =>
+      SongModel(
+        albumadamid: albumadamid ?? this.albumadamid,
+        artists: artists ?? this.artists,
+        genres: genres ?? this.genres,
+        hub: hub ?? this.hub,
+        images: images ?? this.images,
+        isrc: isrc ?? this.isrc,
+        key: key ?? this.key,
+        layout: layout ?? this.layout,
+        relatedtracksurl: relatedtracksurl ?? this.relatedtracksurl,
+        sections: sections ?? this.sections,
+        share: share ?? this.share,
+        subtitle: subtitle ?? this.subtitle,
+        title: title ?? this.title,
+        type: type ?? this.type,
+        url: url ?? this.url,
+        urlparams: urlparams ?? this.urlparams,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'albumadamid': albumadamid,
+        'artists': artists == null
+            ? null
+            : List<dynamic>.from(artists!.map((x) => x.toJson())),
+        'genres': genres?.toJson(),
+        'hub': hub?.toJson(),
+        'images': images?.toJson(),
+        'isrc': isrc,
+        'key': key,
+        'layout': layout,
+        'relatedtracksurl': relatedtracksurl,
+        'sections': sections == null
+            ? null
+            : List<dynamic>.from(sections!.map((x) => x.toJson())),
+        'share': share?.toJson(),
+        'subtitle': subtitle,
+        'title': title,
+        'type': type,
+        'url': url,
+        'urlparams': urlparams?.toJson(),
+      };
+}
diff --git a/shazam_client/lib/src/models/song_model_images.dart b/shazam_client/lib/src/models/song_model_images.dart
new file mode 100644
index 0000000..5d25847
--- /dev/null
+++ b/shazam_client/lib/src/models/song_model_images.dart
@@ -0,0 +1,50 @@
+import 'dart:convert';
+
+class SongModelImages {
+  SongModelImages({
+    this.background,
+    this.coverart,
+    this.coverarthq,
+    this.joecolor,
+  });
+
+  factory SongModelImages.fromRawJson(String str) =>
+      SongModelImages.fromJson(json.decode(str) as Map);
+
+  factory SongModelImages.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return SongModelImages(
+      background: json['background'] as String?,
+      coverart: json['coverart'] as String?,
+      coverarthq: json['coverarthq'] as String?,
+      joecolor: json['joecolor'] as String?,
+    );
+  }
+  final String? background;
+  final String? coverart;
+  final String? coverarthq;
+  final String? joecolor;
+
+  SongModelImages copyWith({
+    String? background,
+    String? coverart,
+    String? coverarthq,
+    String? joecolor,
+  }) =>
+      SongModelImages(
+        background: background ?? this.background,
+        coverart: coverart ?? this.coverart,
+        coverarthq: coverarthq ?? this.coverarthq,
+        joecolor: joecolor ?? this.joecolor,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        'background': background,
+        'coverart': coverart,
+        'coverarthq': coverarthq,
+        'joecolor': joecolor,
+      };
+}
diff --git a/shazam_client/lib/src/models/urlparams.dart b/shazam_client/lib/src/models/urlparams.dart
new file mode 100644
index 0000000..28571e5
--- /dev/null
+++ b/shazam_client/lib/src/models/urlparams.dart
@@ -0,0 +1,38 @@
+import 'dart:convert';
+
+class Urlparams {
+  Urlparams({
+    this.trackartist,
+    this.tracktitle,
+  });
+
+  factory Urlparams.fromRawJson(String str) =>
+      Urlparams.fromJson(json.decode(str) as Map);
+
+  factory Urlparams.fromJson(Map<dynamic, dynamic> json) {
+    json = json.cast<String, dynamic>();
+
+    return Urlparams(
+      trackartist: json['{trackartist}'] as String?,
+      tracktitle: json['{tracktitle}'] as String?,
+    );
+  }
+  final String? trackartist;
+  final String? tracktitle;
+
+  Urlparams copyWith({
+    String? trackartist,
+    String? tracktitle,
+  }) =>
+      Urlparams(
+        trackartist: trackartist ?? this.trackartist,
+        tracktitle: tracktitle ?? this.tracktitle,
+      );
+
+  String toRawJson() => json.encode(toJson());
+
+  Map<String, dynamic> toJson() => {
+        '{trackartist}': trackartist,
+        '{tracktitle}': tracktitle,
+      };
+}
diff --git a/shazam_client/lib/src/shazam_client.dart b/shazam_client/lib/src/shazam_client.dart
new file mode 100644
index 0000000..d295151
--- /dev/null
+++ b/shazam_client/lib/src/shazam_client.dart
@@ -0,0 +1,72 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:http/http.dart' as http;
+import 'package:shazam_client/shazam_client.dart';
+import 'package:shazam_client/src/shazam_client_base.dart';
+
+/// {@template shazam_client}
+/// A Very Good Project created by Very Good CLI.
+/// {@endtemplate}
+class ShazamClient extends ShazamApiClientBase {
+  /// {@macro shazam_client}
+  ShazamClient.localhost({
+    super.timeout = const Duration(seconds: 10),
+  })  : _baseUrl = 'localhost',
+        _port = 5000;
+
+  /// {@macro shazam_client}
+  ShazamClient.dockerized({
+    super.timeout = const Duration(seconds: 10),
+  })  : _baseUrl = 'shazam_api',
+        _port = 5000;
+
+  final String _baseUrl;
+  final int _port;
+
+  @override
+  String get authority => _baseUrl;
+
+  @override
+  int get port => _port;
+
+  /// Recognizes a song from a given [song].
+  Future<SongModel> recognizeSong(File song) async {
+    final uri = Uri(
+      scheme: 'http',
+      host: authority,
+      port: port,
+      path: '/recognize',
+    );
+
+    try {
+      final request = http.MultipartRequest('GET', uri)
+        ..files.add(
+          http.MultipartFile(
+            'file',
+            song.readAsBytes().asStream(),
+            song.lengthSync(),
+            filename: song.path.split('/').last,
+          ),
+        );
+
+      final streamedResponse = await request.send();
+      final response = await http.Response.fromStream(streamedResponse);
+
+      if (response.isFailure) {
+        throw HttpException();
+      }
+
+      return SongModel.fromJson(
+        (jsonDecode(response.body) as Map).cast(),
+      );
+    } on SocketException {
+      rethrow;
+    } on TimeoutException {
+      rethrow;
+    } catch (e) {
+      throw HttpException();
+    }
+  }
+}
diff --git a/shazam_client/lib/src/shazam_client_base.dart b/shazam_client/lib/src/shazam_client_base.dart
new file mode 100644
index 0000000..544c46b
--- /dev/null
+++ b/shazam_client/lib/src/shazam_client_base.dart
@@ -0,0 +1,249 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+import 'package:shazam_client/shazam_client.dart';
+
+/// ShazamApiClientBase is the base class for all API Requests available.
+@internal
+abstract class ShazamApiClientBase {
+  ShazamApiClientBase({
+    required this.timeout,
+  });
+
+  /// The timeout for all API requests.
+  /// Only exposed for testing purposes. Do not use directly.
+  @visibleForTesting
+  final Duration timeout;
+
+  /// The host URL used for all API requests.
+  ///
+  /// Only exposed for testing purposes. Do not use directly.
+  @visibleForTesting
+  String get authority;
+
+  /// The port to which the API requests are made.
+  /// Only exposed for testing purposes. Do not use directly.
+  /// Defaults to 5000.
+  @visibleForTesting
+  int get port => 5000;
+
+  /// The http client used for all API requests.
+  ///
+  /// Only exposed for testing purposes. Do not use directly.
+  @visibleForTesting
+  final http.Client httpClient = http.Client();
+
+  Future<T> post<T>(
+    Uri uri, {
+    Object? body,
+    Map<String, String>? queryParams,
+    Map<String, String>? headers,
+    bool needsToken = true,
+  }) async {
+    assert(
+      body is Map || body is List || body == null,
+      'Invalid body type. Only Map, List or null are allowed. '
+      'Got: ${body.runtimeType}',
+    );
+
+    http.Response response;
+
+    try {
+      response = await httpClient.post(
+        uri,
+        body: (body != null) ? jsonEncode(body) : null,
+        headers: {
+          if (needsToken) ...headersAlways,
+          'needs-token': '$needsToken',
+          ...?headers,
+        },
+      );
+    } on SocketException {
+      rethrow;
+    } on TimeoutException {
+      rethrow;
+    } catch (_) {
+      throw HttpException();
+    }
+
+    return _handleResponse<T>(response);
+  }
+
+  Future<T> patch<T>(
+    Uri uri, {
+    Map<String, dynamic>? body,
+    Map<String, String>? queryParams,
+    Map<String, String>? headers,
+    bool needsToken = true,
+  }) async {
+    http.Response response;
+
+    try {
+      response = await httpClient.patch(
+        uri,
+        body: (body != null) ? jsonEncode(body) : null,
+        headers: {
+          if (needsToken) ...headersAlways,
+          'needs-token': '$needsToken',
+          ...?headers,
+        },
+      );
+    } on SocketException {
+      rethrow;
+    } on TimeoutException {
+      rethrow;
+    } catch (_) {
+      throw HttpException();
+    }
+
+    return _handleResponse<T>(response);
+  }
+
+  Future<T> get<T>(
+    Uri uri, {
+    bool needsToken = true,
+    Map<String, String>? headers,
+  }) async {
+    http.Response response;
+
+    try {
+      response = await httpClient.get(
+        uri,
+        headers: {
+          if (needsToken) ...headersAlways,
+          'needs-token': '$needsToken',
+          ...?headers,
+        },
+      );
+    } on SocketException {
+      rethrow;
+    } on TimeoutException {
+      rethrow;
+    } catch (_) {
+      throw HttpException();
+    }
+
+    return _handleResponse<T>(response);
+  }
+
+  Future<T> put<T>(
+    Uri uri, {
+    Map<String, dynamic>? body,
+    bool needsToken = true,
+    Map<String, String>? headers,
+  }) async {
+    http.Response response;
+
+    try {
+      response = await httpClient.put(
+        uri,
+        body: (body != null) ? jsonEncode(body) : null,
+        headers: {
+          if (needsToken) ...headersAlways,
+          'needs-token': '$needsToken',
+          ...?headers,
+        },
+      );
+    } on SocketException {
+      rethrow;
+    } on TimeoutException {
+      rethrow;
+    } catch (_) {
+      throw HttpException();
+    }
+
+    return _handleResponse<T>(response);
+  }
+
+  Future<T> delete<T>(
+    Uri uri, {
+    Map<String, dynamic>? body,
+    bool needsToken = true,
+    Map<String, String>? headers,
+  }) async {
+    http.Response response;
+
+    try {
+      response = await httpClient.delete(
+        uri,
+        body: (body != null) ? jsonEncode(body) : null,
+        headers: {
+          if (needsToken) ...headersAlways,
+          'needs-token': '$needsToken',
+          ...?headers,
+        },
+      );
+    } on SocketException {
+      rethrow;
+    } on TimeoutException {
+      rethrow;
+    } catch (_) {
+      throw HttpException();
+    }
+
+    return _handleResponse<T>(response);
+  }
+
+  T _handleResponse<T>(http.Response response) {
+    try {
+      if (response is T) return response as T;
+
+      final dynamic decodedResponse = jsonDecode(response.body);
+
+      if (response.isFailure && decodedResponse is Map<String, dynamic>) {
+        if (ExceptionResponse.matches(response)) {
+          throw ExceptionResponse.fromJson(decodedResponse);
+        }
+
+        throw HttpRequestFailure(
+          response.statusCode,
+          response.reasonPhrase ?? '',
+        );
+      }
+
+      if (decodedResponse is T) return decodedResponse;
+
+      try {
+        if (T == JSON) {
+          (decodedResponse as Map).cast<String, dynamic>() as T;
+        } else if (T == JSONLIST) {
+          final newResponse = (decodedResponse as List)
+              .map<JSON>(
+                (dynamic item) => (item as Map).cast<String, dynamic>(),
+              )
+              .toList();
+          return newResponse as T;
+        }
+
+        return decodedResponse as T;
+      } catch (_) {
+        throw const SpecifiedTypeNotMatchedException();
+      }
+    } on FormatException {
+      throw const JsonDecodeException();
+    }
+  }
+
+  /// Closes the http_interceptor client.
+  void close() {
+    httpClient.close();
+  }
+
+  @internal
+  Map<String, String> get headersAlways => <String, String>{
+        'accept': 'application/json',
+        'Content-Type': 'application/json',
+      };
+}
+
+/// A class to make it easier to handle the response from the API.
+extension Result on http.Response {
+  /// Returns true if the response is a success.
+  bool get isSuccess => statusCode >= 200 && statusCode < 300;
+
+  /// Returns true if the response is a failure.
+  bool get isFailure => !isSuccess;
+}
diff --git a/shazam_client/pubspec.yaml b/shazam_client/pubspec.yaml
new file mode 100644
index 0000000..9c95b5f
--- /dev/null
+++ b/shazam_client/pubspec.yaml
@@ -0,0 +1,16 @@
+name: shazam_client
+description: The Dart client for the Shazam API, used by Radio Horizon.
+version: 0.1.0+1
+publish_to: none
+
+environment:
+  sdk: ^3.4.0
+
+dev_dependencies:
+  mocktail: ^1.0.4
+  test: ^1.25.7
+  very_good_analysis: ^6.0.0
+dependencies:
+  equatable: ^2.0.5
+  http: ^1.2.2
+  meta: ^1.15.0
diff --git a/shazam_client/test/src/shazam_client_test.dart b/shazam_client/test/src/shazam_client_test.dart
new file mode 100644
index 0000000..c770613
--- /dev/null
+++ b/shazam_client/test/src/shazam_client_test.dart
@@ -0,0 +1,11 @@
+// ignore_for_file: prefer_const_constructors
+import 'package:shazam_client/shazam_client.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('ShazamClient', () {
+    test('can be instantiated', () {
+      expect(ShazamClient.localhost(), isNotNull);
+    });
+  });
+}
diff --git a/test/radio_recognizer_test.dart b/test/radio_recognizer_test.dart
new file mode 100644
index 0000000..3bcd184
--- /dev/null
+++ b/test/radio_recognizer_test.dart
@@ -0,0 +1,54 @@
+import 'package:radio_horizon/radio_horizon.dart';
+import 'package:retry/retry.dart';
+import 'package:sentry/sentry.dart';
+import 'package:sentry_logging/sentry_logging.dart';
+import 'package:shazam_client/shazam_client.dart';
+import 'package:test/test.dart';
+
+void main() {
+  var recognitionSampleDuration = 10;
+
+  setUpAll(() async {
+    dotEnvFlavour = DotEnvFlavour.development;
+    dotEnvFlavour.initialize();
+
+    await Sentry.init(
+      (options) {
+        options
+          ..dsn = sentryDsn
+          ..environment = dotEnvFlavour.name
+          ..release = packageVersion
+          ..debug = dotEnvFlavour == DotEnvFlavour.development
+          ..attachStacktrace = true
+          ..sampleRate = 1.0
+          ..sendDefaultPii = true
+          ..tracesSampleRate = 1.0
+          ..addIntegration(LoggingIntegration());
+      },
+    );
+  });
+
+  test(
+    'test description',
+    () async {
+      SongModel? result;
+      await retry(
+        () async {
+          result = await SongRecognitionService.instance.identify(
+            'https://ais-edge49-nyc04.cdnstream.com/2281_128.mp3',
+            recognitionSampleDuration,
+          );
+        },
+        maxDelay: const Duration(minutes: 2),
+        retryIf: (e) => true,
+        onRetry: (e) {
+          recognitionSampleDuration +=
+              (recognitionSampleDuration * 0.25).toInt();
+        },
+      ).timeout(const Duration(minutes: 1));
+
+      expect(result, isNotNull);
+    },
+    timeout: const Timeout(Duration(minutes: 2)),
+  );
+}
diff --git a/tool/identify_song.dart b/tool/identify_song.dart
index ff0b06c..9139512 100644
--- a/tool/identify_song.dart
+++ b/tool/identify_song.dart
@@ -1,59 +1,22 @@
 // ignore_for_file: avoid_print
 
-import 'dart:convert';
-import 'dart:developer';
 import 'dart:io';
-import 'dart:typed_data';
 
-import 'package:http/http.dart' as http;
 import 'package:radio_horizon/radio_horizon.dart';
-
-Future<ShazamSongRecognition> identify(Uint8List data) async {
-  final sample = data.buffer.asUint8List();
-
-  final uri = Uri(
-    scheme: 'https',
-    host: 'shazam-song-recognizer.p.rapidapi.com',
-    path: 'recognize',
-  );
-
-  final request = http.MultipartRequest('POST', uri);
-
-  request.headers.addAll({
-    'X-RapidAPI-Key': 'YOUR_KEY',
-    'X-RapidAPI-Host': 'shazam-song-recognizer.p.rapidapi.com',
-  });
-
-  request.files.add(
-    http.MultipartFile.fromBytes(
-      'upload_file',
-      sample,
-    ),
-  );
-
-  final streamedResponse = await request.send();
-  final response = await http.Response.fromStream(streamedResponse);
-  return ShazamSongRecognition.fromJson(
-    (jsonDecode(response.body) as Map).cast(),
-  );
-}
+import 'package:shazam_client/shazam_client.dart';
 
 Future<void> main(List<String> args) async {
   dotEnvFlavour = DotEnvFlavour.development;
   dotEnvFlavour.initialize();
 
   final stopwatch = Stopwatch()..start();
-  log('Recognizing song...');
+  print('Recognizing song...');
 
-  final fileContents = File('sample.mp3').readAsBytesSync();
-  final result = await identify(fileContents);
+  final fileContents = File('sample.mp3');
+  final result = await ShazamClient.dockerized().recognizeSong(fileContents);
 
-  log('Done in ${stopwatch.elapsedMilliseconds}ms');
-  final track = result.result;
-  log(
-    track == null
-        ? 'No song found'
-        : 'Song found: ${'${track.title} - ${track.subtitle}'}',
-  );
-  log(track!.share!.image!);
+  print('Done in ${stopwatch.elapsedMilliseconds}ms');
+  final track = result;
+  print('Song found: ${'${track.title} - ${track.subtitle}'}');
+  print(track.share!.image);
 }