From a235a7d7a4e74ef3c9e73411cef691d5dec1c7cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Sasovsky?= Date: Thu, 1 Aug 2024 05:03:20 +0200 Subject: [PATCH] feat: getIt + bugfixes --- bin/radio_horizon_development.dart | 24 ++++- bin/radio_horizon_production.dart | 27 ++++- lib/src/commands/commands.dart | 11 +- lib/src/commands/info.dart | 5 +- lib/src/commands/music.dart | 16 ++- lib/src/commands/radio.dart | 44 ++++---- lib/src/commands/sound.dart | 28 +++-- lib/src/services/bootup.dart | 92 +++++++---------- lib/src/services/db.dart | 16 +-- lib/src/services/music.dart | 138 ++++++++++++------------- lib/src/services/song_recognition.dart | 94 +++++------------ pubspec.yaml | 1 + test/radio_recognizer_test.dart | 3 +- tool/generate_sample.dart | 54 +++++----- 14 files changed, 282 insertions(+), 271 deletions(-) diff --git a/bin/radio_horizon_development.dart b/bin/radio_horizon_development.dart index 1b10464..654c5a4 100644 --- a/bin/radio_horizon_development.dart +++ b/bin/radio_horizon_development.dart @@ -4,9 +4,13 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:get_it/get_it.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; import 'package:radio_horizon/radio_horizon.dart'; +import 'package:shazam_client/shazam_client.dart'; + +final getIt = GetIt.instance; Future main() async { dotEnvFlavour = DotEnvFlavour.development; @@ -44,12 +48,24 @@ Future main() async { ..registerPlugin(IgnoreExceptions()) ..registerPlugin(commands); - // Initialise our services - MusicService.init(client); - await DatabaseService.init(client); + final databaseService = DatabaseService(client); + await databaseService.initialize(); + + final musicService = MusicService(client); + final bootupService = + BootUpService(client: client, databaseService: databaseService); + final songRecognitionService = + SongRecognitionService(ShazamClient.dockerized()); + + getIt + ..registerSingleton(musicService) + ..registerSingleton(databaseService) + ..registerSingleton(bootupService) + ..registerSingleton(songRecognitionService); client.onReady.listen((_) async { - BootUpService.init(client, DatabaseService.instance); + await musicService.initialize(); + await bootupService.initialize(musicService.cluster); }); // Connect diff --git a/bin/radio_horizon_production.dart b/bin/radio_horizon_production.dart index 011ecd7..38b025a 100644 --- a/bin/radio_horizon_production.dart +++ b/bin/radio_horizon_production.dart @@ -6,12 +6,16 @@ import 'dart:async'; +import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; import 'package:radio_horizon/radio_horizon.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry_logging/sentry_logging.dart'; +import 'package:shazam_client/shazam_client.dart'; + +final getIt = GetIt.instance; Future main() async { await runZonedGuarded(() async { @@ -59,10 +63,25 @@ Future main() async { ..registerPlugin(IgnoreExceptions()) ..registerPlugin(commands); - // Initialise our services - MusicService.init(client); - await DatabaseService.init(client); - BootUpService.init(client, DatabaseService.instance); + final databaseService = DatabaseService(client); + await databaseService.initialize(); + + final musicService = MusicService(client); + final bootupService = + BootUpService(client: client, databaseService: databaseService); + final songRecognitionService = + SongRecognitionService(ShazamClient.dockerized()); + + getIt + ..registerSingleton(musicService) + ..registerSingleton(databaseService) + ..registerSingleton(bootupService) + ..registerSingleton(songRecognitionService); + + client.onReady.listen((_) async { + await musicService.initialize(); + await bootupService.initialize(musicService.cluster); + }); // Connect await client.connect(); diff --git a/lib/src/commands/commands.dart b/lib/src/commands/commands.dart index b8080b0..0446b68 100644 --- a/lib/src/commands/commands.dart +++ b/lib/src/commands/commands.dart @@ -4,6 +4,7 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:get_it/get_it.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; import 'package:nyxx_interactions/nyxx_interactions.dart'; @@ -14,12 +15,16 @@ export 'music.dart'; export 'radio.dart'; export 'sound.dart'; +final _getIt = GetIt.instance; + Future connectIfNeeded( IChatContext context, { bool replace = false, }) async { if (replace) { - MusicService.instance.cluster + _getIt + .get() + .cluster .getOrCreatePlayerNode(context.guild!.id) .destroy(context.guild!.id); context.guild!.shard.changeVoiceState( @@ -51,7 +56,9 @@ Future connectToChannel( bool replace = false, }) async { if (replace) { - MusicService.instance.cluster + _getIt + .get() + .cluster .getOrCreatePlayerNode(guild.id) .destroy(guild.id); guild.shard.changeVoiceState( diff --git a/lib/src/commands/info.dart b/lib/src/commands/info.dart index bdac1a9..1abc01f 100644 --- a/lib/src/commands/info.dart +++ b/lib/src/commands/info.dart @@ -6,6 +6,7 @@ import 'dart:io'; +import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; @@ -22,13 +23,15 @@ String getCurrentMemoryString() { final _enInfoCommand = AppLocale.en.translations.commands.info; final _logger = Logger('command/info'); +final _getIt = GetIt.instance; + ChatCommand info = ChatCommand( _enInfoCommand.command, _enInfoCommand.description, id('info', (IChatContext context) async { context as InteractionChatContext; final commandTranslations = getCommandTranslations(context).info; - final nodes = MusicService.instance.cluster.connectedNodes; + final nodes = _getIt.get().cluster.connectedNodes; final players = nodes.values .map((b) => b.players.length) .reduce((value, element) => value + element); diff --git a/lib/src/commands/music.dart b/lib/src/commands/music.dart index 6098d5a..f0bc043 100644 --- a/lib/src/commands/music.dart +++ b/lib/src/commands/music.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'dart:math' as math; +import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; @@ -18,6 +19,7 @@ final _enMusicCommand = AppLocale.en.translations.commands.music; final _enPlayCommand = _enMusicCommand.children.play; final _logger = Logger('command/music'); +final _getIt = GetIt.instance; ChatGroup music = ChatGroup( _enMusicCommand.command, @@ -53,7 +55,9 @@ ChatCommand:music-play: { }''', ); - final node = MusicService.instance.cluster + final node = _getIt + .get() + .cluster .getOrCreatePlayerNode(context.guild!.id); await connectIfNeeded(context); final result = await node.autoSearch(query); @@ -106,7 +110,9 @@ ChatCommand:music-play: { ); } - await DatabaseService.instance.deleteRadioFromList(context.guild!.id); + await _getIt + .get() + .deleteRadioFromList(context.guild!.id); }), localizedDescriptions: localizedValues( (translations) => translations.commands.music.children.play.description, @@ -141,8 +147,10 @@ ChatCommand:music-play: autocompletion: { }''', ); - final node = - MusicService.instance.cluster.getOrCreatePlayerNode(context.guild!.id); + final node = _getIt + .get() + .cluster + .getOrCreatePlayerNode(context.guild!.id); final response = await node.autoSearch(query).timeout(const Duration(milliseconds: 2500)); diff --git a/lib/src/commands/radio.dart b/lib/src/commands/radio.dart index 2f736b5..dfb5233 100644 --- a/lib/src/commands/radio.dart +++ b/lib/src/commands/radio.dart @@ -22,6 +22,7 @@ import 'package:shazam_client/shazam_client.dart'; final _enRadioCommand = AppLocale.en.translations.commands.radio; final _enPlayCommand = _enRadioCommand.children.play; +final _enPlayRandomCommand = _enRadioCommand.children.playRandom; final _enRecognizeCommand = _enRadioCommand.children.recognize; final _enUpvoteCommand = _enRadioCommand.children.upvote; @@ -46,7 +47,7 @@ ChatGroup radio = ChatGroup( ChatCommand( _enPlayCommand.command, _enPlayCommand.description, - id('radio-play', ( + id('radioplay', ( IChatContext context, @Description('The name of the Radio Station to play') @Autocomplete(autocompleteRadioQuery) @@ -76,7 +77,9 @@ ChatCommand:radio-play: { await connectIfNeeded(context, replace: true); - final node = MusicService.instance.cluster + final node = getIt + .get() + .cluster .getOrCreatePlayerNode(context.guild!.id); late final RadioBrowserListResponse stations; @@ -125,7 +128,7 @@ ChatCommand:radio-play: { channelId: context.channel.id, ).startPlaying(); - final databaseService = DatabaseService.instance; + final databaseService = getIt.get(); await databaseService.setCurrentRadio( context.guild!.id, context.member!.voiceState!.channel!.id, @@ -151,8 +154,8 @@ ChatCommand:radio-play: { ), ), ChatCommand( - _enPlayCommand.command, - _enPlayCommand.description, + _enPlayRandomCommand.command, + _enPlayRandomCommand.description, id('radio-play-random', ( IChatContext context, ) async { @@ -187,7 +190,9 @@ ChatCommand:radio-play-random: { await connectIfNeeded(context, replace: true); - final node = MusicService.instance.cluster + final node = getIt + .get() + .cluster .getOrCreatePlayerNode(context.guild!.id); await _radioBrowserClient.clickStation( @@ -215,12 +220,12 @@ ChatCommand:radio-play-random: { channelId: context.channel.id, ).startPlaying(); - await DatabaseService.instance.setCurrentRadio( - context.guild!.id, - context.member!.voiceState!.channel!.id, - context.channel.id, - radio, - ); + await getIt.get().setCurrentRadio( + context.guild!.id, + context.member!.voiceState!.channel!.id, + context.channel.id, + radio, + ); final embed = EmbedBuilder() ..color = getRandomColor() @@ -233,10 +238,12 @@ ChatCommand:radio-play-random: { await context.respond(MessageBuilder.embed(embed)); }), localizedDescriptions: localizedValues( - (translations) => translations.commands.radio.children.play.description, + (translations) => + translations.commands.radio.children.playRandom.description, ), localizedNames: localizedValues( - (translations) => translations.commands.radio.children.play.command, + (translations) => + translations.commands.radio.children.playRandom.command, ), ), ChatCommand( @@ -251,8 +258,8 @@ ChatCommand:radio-play-random: { CurrentStationInfo? stationInfo; try { - final recognitionService = SongRecognitionService.instance; - final databaseService = DatabaseService.instance; + final recognitionService = getIt.get(); + final databaseService = getIt.get(); final guildId = context.guild!.id; var recognitionSampleDuration = 10; @@ -363,8 +370,9 @@ ChatCommand:radio-play-random: { late GuildRadio? guildRadio; try { - guildRadio = - await DatabaseService.instance.currentRadio(context.guild!.id); + guildRadio = await getIt + .get() + .currentRadio(context.guild!.id); } on RadioNotPlayingException { await context.respond( MessageBuilder.embed( diff --git a/lib/src/commands/sound.dart b/lib/src/commands/sound.dart index d453164..9697e39 100644 --- a/lib/src/commands/sound.dart +++ b/lib/src/commands/sound.dart @@ -33,8 +33,10 @@ ChatCommand:skip: { }''', ); - final node = - MusicService.instance.cluster.getOrCreatePlayerNode(context.guild!.id); + final node = getIt + .get() + .cluster + .getOrCreatePlayerNode(context.guild!.id); final player = node.players[context.guild!.id]!; if (player.queue.isEmpty) { @@ -78,7 +80,9 @@ ChatCommand:leave: { }''', ); - MusicService.instance.cluster + getIt + .get() + .cluster .getOrCreatePlayerNode(context.guild!.id) .destroy(context.guild!.id); context.guild!.shard.changeVoiceState(context.guild!.id, null); @@ -113,7 +117,7 @@ ChatCommand:join: { }''', ); - MusicService.instance.cluster.getOrCreatePlayerNode(context.guild!.id); + getIt.get().cluster.getOrCreatePlayerNode(context.guild!.id); await connectIfNeeded(context); await context.respond(MessageBuilder.content(commandTranslations.joined)); }), @@ -154,7 +158,9 @@ ChatCommand:volume: { }''', ); - MusicService.instance.cluster + getIt + .get() + .cluster .getOrCreatePlayerNode(context.guild!.id) .volume( context.guild!.id, @@ -195,7 +201,9 @@ ChatCommand:pause: { }''', ); - MusicService.instance.cluster + getIt + .get() + .cluster .getOrCreatePlayerNode(context.guild!.id) .pause(context.guild!.id); await context.respond(MessageBuilder.content(commandTranslations.paused)); @@ -228,7 +236,9 @@ ChatCommand:resume: { }''', ); - MusicService.instance.cluster + getIt + .get() + .cluster .getOrCreatePlayerNode(context.guild!.id) .resume(context.guild!.id); await context.respond(MessageBuilder.content(commandTranslations.resumed)); @@ -260,7 +270,9 @@ ChatCommand:stop: { }''', ); - MusicService.instance.cluster + getIt + .get() + .cluster .getOrCreatePlayerNode(context.guild!.id) .stop(context.guild!.id); await context.respond(MessageBuilder.content(commandTranslations.stopped)); diff --git a/lib/src/services/bootup.dart b/lib/src/services/bootup.dart index ee6736e..b1f3d81 100644 --- a/lib/src/services/bootup.dart +++ b/lib/src/services/bootup.dart @@ -7,49 +7,30 @@ import 'package:radio_horizon/radio_horizon.dart'; import 'package:retry/retry.dart'; class BootUpService { - BootUpService._(this._client, this.databaseService) { - try { - unawaited(_initialize()); - } catch (e, s) { - Logger('BootUpService').severe('Error during bootup', e, s); - } - } - - static void init(INyxxWebsocket client, DatabaseService databaseService) { - _instance = BootUpService._( - client, - databaseService, - ); - } - - static BootUpService get instance => - _instance ?? - (throw Exception( - 'DB service must be initialised with DB.init', - )); - - static BootUpService? _instance; + BootUpService({ + required INyxxWebsocket client, + required this.databaseService, + }) : _client = client; final DatabaseService databaseService; final INyxxWebsocket _client; /// Grabs the previously playing radio and sets it as the current one /// for the guild - Future _initialize() async { - final cluster = await retry( + Future initialize(ICluster cluster) async { + final mCluster = await retry( () async { - final mCluster = MusicService.instance.cluster; - if (mCluster.connectedNodes.isEmpty) { + if (cluster.connectedNodes.isEmpty) { throw Exception('No nodes connected yet... retrying'); } else { - return mCluster; + return cluster; } }, maxAttempts: 10, delayFactor: const Duration(seconds: 5), ); - if (cluster == null || cluster.connectedNodes.isEmpty) { + if (mCluster == null || mCluster.connectedNodes.isEmpty) { return; } @@ -60,35 +41,38 @@ class BootUpService { } for (final playing in allPlaying) { - final guild = await _client.fetchGuild(playing.guildId); + try { + final guild = await _client.fetchGuild(playing.guildId); - await connectToChannel( - guild, - playing.voiceChannelId, - replace: true, - ); - - final node = - MusicService.instance.cluster.getOrCreatePlayerNode(guild.id); - final tracks = await node - .searchTracks(playing.station.urlResolved ?? playing.station.url); - node - ..players[guild.id]!.queue.clear() - ..play( - guild.id, - tracks.tracks.first, + await connectToChannel( + guild, + playing.voiceChannelId, replace: true, - channelId: playing.voiceChannelId, - ).startPlaying(); + ); + + final node = mCluster.getOrCreatePlayerNode(guild.id); + final tracks = await node + .searchTracks(playing.station.urlResolved ?? playing.station.url); + node + ..players[guild.id]!.queue.clear() + ..play( + guild.id, + tracks.tracks.first, + replace: true, + channelId: playing.voiceChannelId, + ).startPlaying(); - await databaseService.setPlaying( - GuildRadio( - guild.id, - voiceChannelId: playing.voiceChannelId, - station: playing.station, - textChannelId: playing.textChannelId, - ), - ); + await databaseService.setPlaying( + GuildRadio( + guild.id, + voiceChannelId: playing.voiceChannelId, + station: playing.station, + textChannelId: playing.textChannelId, + ), + ); + } catch (e) { + Logger('BootUpService').shout('Error during bootup', e); + } } } } diff --git a/lib/src/services/db.dart b/lib/src/services/db.dart index 77a0783..a72f142 100644 --- a/lib/src/services/db.dart +++ b/lib/src/services/db.dart @@ -7,19 +7,12 @@ import 'package:radio_browser_api/radio_browser_api.dart'; import 'package:radio_horizon/radio_horizon.dart'; class DatabaseService { - DatabaseService._(this._client) { + DatabaseService(this._client) { _client.onReady.listen((_) async { await _addServer(); }); } - static DatabaseService get instance => - _instance ?? - (throw Exception( - 'DB service must be initialised with DB.init', - )); - - static DatabaseService? _instance; late Db _db; Future _checkConnection() async { @@ -28,7 +21,7 @@ class DatabaseService { } } - Future _initialize() async { + Future initialize() async { /// Connects to the MongoDB database _db = await Db.create(mongoDBConnection); await _db.open(); @@ -164,10 +157,5 @@ class DatabaseService { final INyxxWebsocket _client; - static Future init(INyxxWebsocket client) { - _instance = DatabaseService._(client); - return _instance!._initialize(); - } - FutureOr close() async => _db.close(); } diff --git a/lib/src/services/music.dart b/lib/src/services/music.dart index 5cad787..0ed8258 100644 --- a/lib/src/services/music.dart +++ b/lib/src/services/music.dart @@ -4,85 +4,81 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_lavalink/nyxx_lavalink.dart'; import 'package:radio_horizon/radio_horizon.dart'; final _enMusicService = AppLocale.en.translations.services.music; +final getIt = GetIt.instance; class MusicService { - MusicService._(this._client) { - _client.onReady.listen((_) async { - if (_cluster == null) { - _cluster = ICluster.createCluster(_client, _client.appId); - - final host = serverAddress; - final port = serverPort; - final password = serverPassword; - final ssl = useSSL; - final shards = _client.shardManager.totalNumShards; - - logger.info( - 'Connecting to Lavalink server at $host:$port (SSL: $ssl, ' - 'Shards: $shards)', - ); + MusicService(this._client); - await cluster.addNode( - NodeOptions( - host: host, - port: port, - password: password, - ssl: ssl, - clientName: 'RadioHorizon', - shards: shards, - // Bump up connection attempts to avoid timeouts in Docker - maxConnectAttempts: 10, - ), - ); + final logger = Logger('MusicService'); + final INyxxWebsocket _client; - for (var i = 0; i < 10; i++) { - if (cluster.connectedNodes.isNotEmpty) { - break; - } + ICluster get cluster => + _cluster ?? + (throw Exception('Cluster must be accessed after `on_ready` event')); - await Future.delayed(const Duration(seconds: 5)); - } + /// Initializes the music service + Future initialize() async { + if (_cluster == null) { + _cluster = ICluster.createCluster(_client, _client.appId); - if (cluster.connectedNodes.isEmpty) { - logger.severe( - 'Failed to connect to Lavalink server at $host:$port (SSL: $ssl, ' - 'Shards: $shards)', - ); + final host = serverAddress; + final port = serverPort; + final password = serverPassword; + final ssl = useSSL; + final shards = _client.shardManager.totalNumShards; + + logger.info( + 'Connecting to Lavalink server at $host:$port (SSL: $ssl, ' + 'Shards: $shards)', + ); - return; + await cluster.addNode( + NodeOptions( + host: host, + port: port, + password: password, + ssl: ssl, + clientName: 'RadioHorizon', + shards: shards, + // Bump up connection attempts to avoid timeouts in Docker + maxConnectAttempts: 10, + ), + ); + + for (var i = 0; i < 10; i++) { + if (cluster.connectedNodes.isNotEmpty) { + break; } - logger.info( - 'MusicService:constructor: lavalink-connection-successful', + await Future.delayed(const Duration(seconds: 5)); + } + + if (cluster.connectedNodes.isEmpty) { + logger.severe( + 'Failed to connect to Lavalink server at $host:$port (SSL: $ssl, ' + 'Shards: $shards)', ); - cluster.eventDispatcher.onTrackStart.listen(_trackStarted); - cluster.eventDispatcher.onTrackStuck.listen(_trackStuck); - cluster.eventDispatcher.onTrackEnd.listen(_trackEnded); - cluster.eventDispatcher.onTrackException.listen(_trackException); + return; } - }); - } - final logger = Logger('MusicService'); - static MusicService get instance => - _instance ?? - (throw Exception( - 'Music service must be initialised with MusicService.init', - )); - static MusicService? _instance; - - final INyxxWebsocket _client; + logger.info( + 'MusicService:constructor: lavalink-connection-successful', + ); - ICluster get cluster => - _cluster ?? - (throw Exception('Cluster must be accessed after `on_ready` event')); + cluster.eventDispatcher.onTrackStart.listen(_trackStarted); + cluster.eventDispatcher.onTrackStuck.listen(_trackStuck); + cluster.eventDispatcher.onTrackEnd.listen(_trackEnded); + cluster.eventDispatcher.onTrackException.listen(_trackException); + } + } /// The cluster used to interact with lavalink ICluster? _cluster; @@ -139,6 +135,17 @@ class MusicService { return; } + final databaseService = getIt.get(); + try { + await databaseService.currentRadio(event.guildId); + // if the current radio is not null, it means the bot is playing a radio + return; + } catch (e) { + logger.severe( + 'MusicService:_trackEnded: failed to get current radio: $e', + ); + } + // disconnect the bot if the queue is empty final player = event.node.players[event.guildId]; if (player != null && player.queue.isEmpty && player.nowPlaying == null) { @@ -155,7 +162,7 @@ class MusicService { guild.shard.changeVoiceState(guild.id, null); // delete the current radio station from the list, if it exists - await DatabaseService.instance.deleteRadioFromList(guild.id); + await getIt.get().deleteRadioFromList(guild.id); logger.info( 'Disconnected from voice channel in guild ${guild.id} ' @@ -231,10 +238,6 @@ class MusicService { } } - static void init(INyxxWebsocket client) { - _instance = MusicService._(client); - } - Future voiceStateUpdate(IVoiceStateUpdateEvent event) async { if (event.state.user.id == _client.appId) return; if (event.oldState == null) return; @@ -292,13 +295,10 @@ class MusicService { if (!hasConnectedMembers(guild)) { try { - MusicService.instance.cluster - .getOrCreatePlayerNode(guild.id) - .destroy(guild.id); - + cluster.getOrCreatePlayerNode(guild.id).destroy(guild.id); guild.shard.changeVoiceState(guild.id, null); - await DatabaseService.instance.setNotPlaying(guild.id); + await getIt.get().setNotPlaying(guild.id); final channel = await guild.publicUpdatesChannel?.getOrDownload(); if (channel == null) { diff --git a/lib/src/services/song_recognition.dart b/lib/src/services/song_recognition.dart index 1fbd0a5..9fb98ea 100644 --- a/lib/src/services/song_recognition.dart +++ b/lib/src/services/song_recognition.dart @@ -15,13 +15,7 @@ import 'package:shazam_client/shazam_client.dart'; import 'package:uuid/uuid.dart'; class SongRecognitionService { - SongRecognitionService._privateConstructor() - : _shazamClient = ShazamClient.dockerized(); - - static SongRecognitionService get instance => _instance; - - static final SongRecognitionService _instance = - SongRecognitionService._privateConstructor(); + SongRecognitionService(this._shazamClient); Uuid get uuid => const Uuid(); @@ -90,85 +84,53 @@ class SongRecognitionService { final completer = Completer(); final uri = Uri.parse(url); - final request = http.Request('GET', uri); - http.StreamedResponse? response; + http.StreamedResponse response; try { response = await httpClient.send(request); } catch (e) { throw RadioCantCommunicateWithServer(Exception(e.toString())); } - StreamSubscription>? streamSubscription; - final responseBitRate = response.headers['icy-br'] ?? '128'; - final bitRate = num.parse(responseBitRate.split(',').first).toInt(); - final outputFile = File('${Directory.systemTemp.path}/${uuid.v4()}'); - if (!outputFile.existsSync()) outputFile.createSync(); - final sink = outputFile.openWrite(); + final outputFile = + File('${Directory.systemTemp.path}/${const Uuid().v4()}.mp3'); + if (!outputFile.existsSync()) await outputFile.create(); - // we can calculate the duration by using the bitrate and - // the file size, the formula is found here: - // http://www.audiomountain.com/tech/audio-file-size.html + final sink = outputFile.openWrite(); final bytePerSecond = bitRate / 8 * 1024; final expectedBytes = bytePerSecond * durationInSeconds; - streamSubscription = response.stream.listen((chunk) async { - final bytes = await outputFile.length(); - - if (bytes > 0) { - if (expectedBytes < bytes) { - unawaited(streamSubscription?.cancel()); - - await sink.flush(); + StreamSubscription>? streamSubscription; + streamSubscription = response.stream.listen( + (chunk) async { + sink.add(chunk); + final bytes = await outputFile.length(); + if (bytes >= expectedBytes) { + await streamSubscription?.cancel(); await sink.close(); - if (!completer.isCompleted) { completer.complete(outputFile); } - return; } - } - - sink.add(chunk); - }); - - return completer.future; - } - - /// Gets a list of links to different streaming services where the song - /// is available. - Future getMusicLinks(String songName) async { - final uri = Uri( - scheme: 'https', - host: 'musiclinkssapi.p.rapidapi.com', - path: 'search/query', - queryParameters: { - 'q': songName, - 'type': 'track', }, + onDone: () async { + await sink.close(); + if (!completer.isCompleted) { + completer.complete(outputFile); + } + }, + onError: (Object error) async { + await streamSubscription?.cancel(); + await sink.close(); + if (!completer.isCompleted) { + completer.completeError(error); + } + }, + cancelOnError: true, ); - final request = http.Request('GET', uri); - - request.headers.addAll({ - 'X-RapidAPI-Key': rapidapiShazamSongRecognizerKey, - 'X-RapidAPI-Host': 'musiclinkssapi.p.rapidapi.com', - }); - - final streamedResponse = await request.send(); - final response = await http.Response.fromStream(streamedResponse); - - final decodedResponse = jsonDecode(response.body); - if (decodedResponse is! Map || decodedResponse.isEmpty) { - return MusicLinksResponse.empty(); - } - - final result = MusicLinksResponse.fromJson( - (jsonDecode(response.body) as Map).cast(), - ); - - return result; + return completer.future; } } diff --git a/pubspec.yaml b/pubspec.yaml index 7ec07a2..ed0befb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ environment: dependencies: crypto: ^3.0.3 dotenv: ^4.1.0 + get_it: ^7.7.0 http: ">=0.13.0 <1.1.0" json_annotation: ^4.8.1 logging: ^1.2.0 diff --git a/test/radio_recognizer_test.dart b/test/radio_recognizer_test.dart index 3bcd184..bbb6187 100644 --- a/test/radio_recognizer_test.dart +++ b/test/radio_recognizer_test.dart @@ -34,7 +34,8 @@ void main() { SongModel? result; await retry( () async { - result = await SongRecognitionService.instance.identify( + result = + await SongRecognitionService(ShazamClient.localhost()).identify( 'https://ais-edge49-nyc04.cdnstream.com/2281_128.mp3', recognitionSampleDuration, ); diff --git a/tool/generate_sample.dart b/tool/generate_sample.dart index 9413668..19b634d 100644 --- a/tool/generate_sample.dart +++ b/tool/generate_sample.dart @@ -11,45 +11,47 @@ Future generateSample({ }) async { final url = radio.urlResolved ?? radio.url; final uri = Uri.parse(url); - final client = http.Client(); final request = http.Request('GET', uri); final response = await client.send(request); - StreamSubscription>? streamSubscription; - - final bitRate = radio.bitrate; - final outputFile = File(outputPath); - if (!outputFile.existsSync()) outputFile.createSync(); - final sink = outputFile.openWrite(); + if (!outputFile.existsSync()) await outputFile.create(); + final sink = outputFile.openWrite(); final completer = Completer(); + final bitRate = radio.bitrate; + final bytePerSecond = bitRate / 8 * 1000; + final expectedBytes = bytePerSecond * durationInSeconds; - streamSubscription = response.stream.listen((chunk) async { - final bytes = await outputFile.length(); - - if (bytes > 0) { - // we can calculate the duration by using the bitrate and the file size, - // the formula is found here: - // http://www.audiomountain.com/tech/audio-file-size.html - final bytePerSecond = bitRate / 8 * 1000; - final expectedBytes = bytePerSecond * durationInSeconds; - - if (expectedBytes < bytes) { - await sink.flush(); - await sink.close(); - + StreamSubscription>? streamSubscription; + streamSubscription = response.stream.listen( + (chunk) async { + sink.add(chunk); + final bytes = await outputFile.length(); + if (bytes >= expectedBytes) { await streamSubscription?.cancel(); + await sink.close(); if (!completer.isCompleted) { completer.complete(); } - return; } - } - - sink.add(chunk); - }); + }, + onDone: () async { + await sink.close(); + if (!completer.isCompleted) { + completer.complete(); + } + }, + onError: (Object error) async { + await streamSubscription?.cancel(); + await sink.close(); + if (!completer.isCompleted) { + completer.completeError(error); + } + }, + cancelOnError: true, + ); return completer.future; }