From 554645ef51b1fccf6cc0a23087568f0a3b878773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Sasovsky?= Date: Sun, 27 Oct 2024 05:00:01 +0100 Subject: [PATCH] feat: nyxx bump (#115) --- .gitignore | 4 +- analysis_options.yaml | 2 + bin/radio_horizon_development.dart | 90 +- bin/radio_horizon_production.dart | 88 +- docker-compose.prod.yml | 30 +- docker-compose.yml | 21 +- lavalink.yml | 78 +- lib/i18n/strings.g.dart | 1875 +---------------- lib/i18n/strings.i18n.json | 19 +- lib/i18n/strings_en.g.dart | 599 ++++++ lib/i18n/strings_es.g.dart | 602 ++++++ lib/i18n/strings_es.i18n.json | 19 +- lib/src/checks.dart | 65 +- lib/src/commands/commands.dart | 89 +- lib/src/commands/info.dart | 202 +- lib/src/commands/music.dart | 330 ++- lib/src/commands/radio.dart | 284 ++- lib/src/commands/sound.dart | 240 +-- lib/src/dotenv.dart | 2 +- lib/src/error_handler.dart | 27 +- lib/src/helpers/music_queue.dart | 65 + lib/src/models/exceptions.dart | 3 + .../music_links_response.dart | 31 - .../models/song_recognition/guild_radio.dart | 6 +- lib/src/requires_initialization.dart | 3 + lib/src/services/bootup.dart | 52 +- lib/src/services/bot_start_duration.dart | 10 + lib/src/services/db.dart | 35 +- lib/src/services/music.dart | 329 --- lib/src/services/services.dart | 2 +- lib/src/services/song_recognition.dart | 8 +- lib/src/settings.dart | 23 +- lib/src/util.dart | 33 +- lib/src/version.dart | 2 +- portainer.yml | 46 + pubspec.yaml | 49 +- shazam_client/analysis_options.yaml | 2 +- test/radio_recognizer_test.dart | 8 +- tool/clean_commands.dart | 55 +- update_lavalink_config/Dockerfile | 20 + update_lavalink_config/pubspec.yaml | 11 + .../update_lavalink_config.dart | 159 ++ 42 files changed, 2581 insertions(+), 3037 deletions(-) create mode 100644 lib/i18n/strings_en.g.dart create mode 100644 lib/i18n/strings_es.g.dart create mode 100644 lib/src/helpers/music_queue.dart create mode 100644 lib/src/models/exceptions.dart create mode 100644 lib/src/requires_initialization.dart create mode 100644 lib/src/services/bot_start_duration.dart delete mode 100644 lib/src/services/music.dart create mode 100644 portainer.yml create mode 100644 update_lavalink_config/Dockerfile create mode 100644 update_lavalink_config/pubspec.yaml create mode 100644 update_lavalink_config/update_lavalink_config.dart diff --git a/.gitignore b/.gitignore index 9299da2..b2d66a1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ coverage/ /shazam_api/__pycache__ /shazam_api/pyvenv.cfg /shazam_api/pyvenv.cfg -.DS_Store \ No newline at end of file +.DS_Store + +output.lavalink.yml diff --git a/analysis_options.yaml b/analysis_options.yaml index 613877c..558e9be 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,6 +4,8 @@ linter: public_member_api_docs: false analyzer: + errors: + cascade_invocations: ignore exclude: - "**/*.g.dart" - "**/*.freezed.dart" diff --git a/bin/radio_horizon_development.dart b/bin/radio_horizon_development.dart index 654c5a4..ca83828 100644 --- a/bin/radio_horizon_development.dart +++ b/bin/radio_horizon_development.dart @@ -1,30 +1,39 @@ -// Copyright (c) 2022, Tomás Sasovsky +// Copyright (c) 2024, Tomás Sasovsky // // Use of this source code is governed by an MIT-style // 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:injector/injector.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; +import 'package:nyxx_lavalink/nyxx_lavalink.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; dotEnvFlavour.initialize(); - // Create nyxx client and nyxx_commands plugin - final client = NyxxFactory.createNyxxWebsocket(token, intents); - final commands = CommandsPlugin( - prefix: mentionOr((_) => prefix), - options: const CommandsOptions( - logErrors: false, + prefix: null, + guild: devGuildId, + options: CommandsOptions(logErrors: dev), + ); + + final lavalinkClient = await LavalinkClient.connect( + Uri( + host: lavalinkAddress, + port: lavalinkPort, + scheme: lavalinkUseSSL ? 'https' : 'http', ), - ) + password: lavalinkPassword, + userId: clientId.toString(), + ); + + final lavalinkPlugin = LavalinkPlugin.usingClient(lavalinkClient); + + commands ..addCommand(info) ..addCommand(skip) ..addCommand(stop) @@ -34,40 +43,35 @@ Future main() async { ..addCommand(resume) ..addCommand(volume) ..addCommand(music) - ..addCommand(radio) - ..onCommandError.listen(commandErrorHandler); - - client - ..registerPlugin( - Logging( - logLevel: Level.FINE, - truncateLogsAt: 10000, - ), - ) - ..registerPlugin(CliIntegration()) - ..registerPlugin(IgnoreExceptions()) - ..registerPlugin(commands); + ..addCommand(radio); - final databaseService = DatabaseService(client); - await databaseService.initialize(); + commands.onCommandError.listen(commandErrorHandler); - 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); + final client = await Nyxx.connectGateway( + token, + intents, + options: GatewayClientOptions( + plugins: [ + Logging(), + CliIntegration(), + IgnoreExceptions(), + commands, + lavalinkPlugin, + ], + ), + ); - client.onReady.listen((_) async { - await musicService.initialize(); - await bootupService.initialize(musicService.cluster); - }); + Injector.appInstance + ..registerSingleton(() => client) + ..registerSingleton(DatabaseService.new) + ..registerSingleton(BootUpService.new) + ..registerSingleton(ShazamClient.dockerized) + ..registerSingleton(SongRecognitionService.new) + ..registerSingleton(BotStartDuration.new) + ..registerSingleton(() => lavalinkClient) + ..registerSingleton(() => lavalinkPlugin); - // Connect - await client.connect(); + await Injector.appInstance.get().init(); + await Injector.appInstance.get().init(); + await Injector.appInstance.get().init(); } diff --git a/bin/radio_horizon_production.dart b/bin/radio_horizon_production.dart index 38b025a..fdd9213 100644 --- a/bin/radio_horizon_production.dart +++ b/bin/radio_horizon_production.dart @@ -6,17 +6,15 @@ import 'dart:async'; -import 'package:get_it/get_it.dart'; -import 'package:logging/logging.dart'; +import 'package:injector/injector.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; +import 'package:nyxx_lavalink/nyxx_lavalink.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 { dotEnvFlavour = DotEnvFlavour.production; @@ -37,8 +35,17 @@ Future main() async { }, ); - // Create nyxx client and nyxx_commands plugin - final client = NyxxFactory.createNyxxWebsocket(token, intents); + final lavalinkClient = await LavalinkClient.connect( + Uri( + host: lavalinkAddress, + port: lavalinkPort, + scheme: lavalinkUseSSL ? 'https' : 'http', + ), + password: lavalinkPassword, + userId: clientId.toString(), + ); + + final lavalinkPlugin = LavalinkPlugin.usingClient(lavalinkClient); final commands = CommandsPlugin( prefix: mentionOr((_) => prefix), @@ -47,44 +54,45 @@ Future main() async { ), ) ..addCommand(info) - ..addCommand(skip) - ..addCommand(stop) - ..addCommand(join) - ..addCommand(leave) - ..addCommand(pause) - ..addCommand(resume) - ..addCommand(volume) - ..addCommand(music) - ..addCommand(radio) - ..onCommandError.listen(commandErrorHandler); + // ..addCommand(skip) + // ..addCommand(stop) + // ..addCommand(join) + // ..addCommand(leave) + // ..addCommand(pause) + // ..addCommand(resume) + // ..addCommand(volume) + ..addCommand(music); + // ..addCommand(radio); - client - ..registerPlugin(CliIntegration()) - ..registerPlugin(IgnoreExceptions()) - ..registerPlugin(commands); + commands.onCommandError.listen(commandErrorHandler); - 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); + final client = await Nyxx.connectGateway( + token, + intents, + options: GatewayClientOptions( + plugins: [ + Logging(), + CliIntegration(), + IgnoreExceptions(), + commands, + lavalinkPlugin, + ], + ), + ); - client.onReady.listen((_) async { - await musicService.initialize(); - await bootupService.initialize(musicService.cluster); - }); + Injector.appInstance + ..registerSingleton(() => client) + ..registerSingleton(DatabaseService.new) + ..registerSingleton(BootUpService.new) + ..registerSingleton(ShazamClient.dockerized) + ..registerSingleton(SongRecognitionService.new) + ..registerSingleton(BotStartDuration.new) + ..registerSingleton(() => lavalinkClient) + ..registerSingleton(() => lavalinkPlugin); - // Connect - await client.connect(); + await Injector.appInstance.get().init(); + await Injector.appInstance.get().init(); + await Injector.appInstance.get().init(); }, (exception, stackTrace) async { Logger('main').severe( 'Uncaught exception when initialising the bot', diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index fc18c40..94ae8ab 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -2,10 +2,11 @@ services: radio_horizon: image: ghcr.io/tomassasovsky/radio-horizon.dart:latest container_name: radio_horizon + restart: unless-stopped expose: - 8080 env_file: - - .env/.env.production + - .env/.env.development links: - lavalink - shazam_api @@ -14,20 +15,33 @@ services: - shazam_api lavalink: - image: ghcr.io/lavalink-devs/lavalink:3 + image: ghcr.io/lavalink-devs/lavalink:4 container_name: lavalink - restart: always + restart: unless-stopped expose: - 2333 volumes: - - ./lavalink.yml:/opt/Lavalink/application.yml + - ./output.lavalink.yml:/opt/Lavalink/application.yml + depends_on: + update_lavalink_config: + condition: service_completed_successfully shazam_api: - build: - context: ./shazam_api + image: ghcr.io/tomassasovsky/shazam_api:latest + container_name: shazam_api + restart: unless-stopped expose: - 5000 - volumes: - - ./shazam_api:/app environment: FLASK_ENV: production + + update_lavalink_config: + image: ghcr.io/tomassasovsky/update-lavalink-config:latest + container_name: update_lavalink_config + restart: no + volumes: + - ./lavalink.yml:/app/lavalink.yml + - ./:/output-dir + env_file: + - .env/.env.development + command: ["--input", "lavalink.yml", "--output", "/output-dir/output.lavalink.yml"] diff --git a/docker-compose.yml b/docker-compose.yml index c9599a6..dd7cda5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,20 +17,33 @@ services: - shazam_api lavalink: - image: ghcr.io/lavalink-devs/lavalink:3 + image: ghcr.io/lavalink-devs/lavalink:4 container_name: lavalink restart: always expose: - 2333 volumes: - - ./lavalink.yml:/opt/Lavalink/application.yml + - ./output.lavalink.yml:/opt/Lavalink/application.yml + depends_on: + update_lavalink_config: + condition: service_completed_successfully shazam_api: build: context: ./shazam_api expose: - 5000 - volumes: - - ./shazam_api:/app environment: FLASK_ENV: development + + update_lavalink_config: + build: + context: ./custom_lavalink + container_name: update_lavalink_config + restart: no + volumes: + - ./lavalink.yml:/app/lavalink.yml + - ./:/output-dir + env_file: + - .env/.env.development + command: ["--input", "lavalink.yml", "--output", "/output-dir/output.lavalink.yml"] diff --git a/lavalink.yml b/lavalink.yml index f3da498..3096a85 100644 --- a/lavalink.yml +++ b/lavalink.yml @@ -3,26 +3,44 @@ server: address: 0.0.0.0 lavalink: server: - password: "youshallnotpass" # pragma: allowlist secret + password: null # pragma: allowlist secret sources: - youtube: true + youtube: false bandcamp: true soundcloud: true twitch: true vimeo: true http: true local: false - bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Minimum of 40ms, lower values may introduce pauses. - frameBufferDurationMs: 5000 # How many milliseconds of audio to keep buffered - opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU. - resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU. - trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data. - useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready. - youtubePlaylistLoadLimit: 6 # Number of pages at 100 each - playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds + # The duration of the NAS buffer. Higher values fare better against + # longer GC pauses. Minimum of 40ms, lower values may introduce pauses. + bufferDurationMs: 1000 + # How many milliseconds of audio to keep buffered + frameBufferDurationMs: 5000 + # Opus encoder quality. Valid values range from 0 to 10, where 10 is best + # quality but is the most expensive on the CPU. + opusEncodingQuality: 10 + # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, + # where HIGH uses the most CPU. + resamplingQuality: MEDIUM + # The threshold for how long a track can be stuck. A track is stuck if does + # not return any audio data. + trackStuckThresholdMs: 10000 + # Seek ghosting is the effect where whilst a seek is in progress, the audio + # buffer is read from until empty, or until seek is ready. + useSeekGhosting: true + # Number of pages at 100 each + youtubePlaylistLoadLimit: 10 + # How frequently to send player updates to clients, in seconds + playerUpdateInterval: 5 youtubeSearchEnabled: true soundcloudSearchEnabled: true gc-warnings: true + plugins: + - dependency: "dev.lavalink.youtube:youtube-plugin:1.8.3" + snapshot: false + - dependency: "com.github.topi314.lavasrc:lavasrc-plugin:4.3.0" + snapshot: false metrics: prometheus: @@ -39,3 +57,43 @@ logging: rollingpolicy: max-file-size: 1GB max-history: 30 + +plugins: + youtube: + enabled: true + allowSearch: true + # Whether just video IDs can match. If false, only complete URLs + # will be loaded. + allowDirectVideoIds: true + # Whether just playlist IDs can match. If false, only complete URLs + # will be loaded. + allowDirectPlaylistIds: true + # The clients to use for track loading. + clients: + - MUSIC + - ANDROID_TESTSUITE + - WEB + - TVHTML5EMBEDDED + oauth: + enabled: true + refreshToken: null + lavasrc: + providers: + - "dzisrc:%ISRC%" + - "dzsearch:%QUERY%" + - "ytsearch:\"%ISRC%\"" + - "ytsearch:%QUERY%" + sources: + spotify: false + applemusic: false + deezer: true + yandexmusic: false + flowerytts: false + youtube: true + deezer: + # the master key used for decrypting the deezer tracks. + # (yes this is not here you need to get it from somewhere else) + masterDecryptionKey: null + formats: [ "MP3_128", "MP3_64" ] + youtube: + countryCode: "US" \ No newline at end of file diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 1a4a6af..b5f9129 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -1,49 +1,91 @@ /// Generated file. Do not edit. /// -/// Original: lib/i18n +/// Source: lib/i18n /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 196 (98 per locale) +/// Strings: 214 (107 per locale) /// -/// Built on 2023-10-01 at 23:21 UTC +/// Built on 2024-10-21 at 21:54 UTC // coverage:ignore-file -// ignore_for_file: type=lint +// ignore_for_file: type=lint, unused_import -import 'package:slang/builder/model/node.dart'; +import 'package:intl/intl.dart'; +import 'package:slang/generated.dart'; import 'package:slang/slang.dart'; export 'package:slang/slang.dart'; -const AppLocale _baseLocale = AppLocale.en; +import 'strings_es.g.dart' deferred as l_es; +part 'strings_en.g.dart'; -/// Supported locales, see extension methods below. +/// Supported locales. /// /// Usage: /// - LocaleSettings.setLocale(AppLocale.en) // set locale /// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum /// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check -enum AppLocale with BaseAppLocale { - en(languageCode: 'en', build: StringsEn.build), - es(languageCode: 'es', build: StringsEs.build); - - const AppLocale( - {required this.languageCode, - this.scriptCode, - this.countryCode, - required this.build}); // ignore: unused_element - - @override - final String languageCode; - @override - final String? scriptCode; - @override - final String? countryCode; - @override - final TranslationBuilder build; - - /// Gets current instance managed by [LocaleSettings]. - StringsEn get translations => LocaleSettings.instance.translationMap[this]!; +enum AppLocale with BaseAppLocale { + en(languageCode: 'en'), + es(languageCode: 'es'); + + const AppLocale({ + required this.languageCode, + this.scriptCode, // ignore: unused_element + this.countryCode, // ignore: unused_element + }); + + @override final String languageCode; + @override final String? scriptCode; + @override final String? countryCode; + + @override + Future build({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) async { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.es: + await l_es.loadLibrary(); + return l_es.TranslationsEs( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + @override + Translations buildSync({ + Map? overrides, + PluralResolver? cardinalResolver, + PluralResolver? ordinalResolver, + }) { + switch (this) { + case AppLocale.en: + return TranslationsEn( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + case AppLocale.es: + return l_es.TranslationsEs( + overrides: overrides, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + } + } + + /// Gets current instance managed by [LocaleSettings]. + Translations get translations => LocaleSettings.instance.getTranslations(this); } /// Method A: Simple @@ -55,1744 +97,51 @@ enum AppLocale with BaseAppLocale { /// Usage: /// String a = local.someKey.anotherKey; /// String b = local['someKey.anotherKey']; // Only for edge cases! -StringsEn get local => LocaleSettings.instance.currentTranslations; +Translations get local => LocaleSettings.instance.currentTranslations; /// Manages all translation instances and the current locale -class LocaleSettings extends BaseLocaleSettings { - LocaleSettings._() : super(utils: AppLocaleUtils.instance); - - static final instance = LocaleSettings._(); - - // static aliases (checkout base methods for documentation) - static AppLocale get currentLocale => instance.currentLocale; - static Stream getLocaleStream() => instance.getLocaleStream(); - static AppLocale setLocale(AppLocale locale, - {bool? listenToDeviceLocale = false}) => - instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); - static AppLocale setLocaleRaw(String rawLocale, - {bool? listenToDeviceLocale = false}) => - instance.setLocaleRaw(rawLocale, - listenToDeviceLocale: listenToDeviceLocale); - @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') - static List get supportedLocalesRaw => instance.supportedLocalesRaw; - static void setPluralResolver( - {String? language, - AppLocale? locale, - PluralResolver? cardinalResolver, - PluralResolver? ordinalResolver}) => - instance.setPluralResolver( - language: language, - locale: locale, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ); +class LocaleSettings extends BaseLocaleSettings { + LocaleSettings._() : super( + utils: AppLocaleUtils.instance, + lazy: true, + ); + + static final instance = LocaleSettings._(); + + // static aliases (checkout base methods for documentation) + static AppLocale get currentLocale => instance.currentLocale; + static Stream getLocaleStream() => instance.getLocaleStream(); + static Future setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static Future setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static Future setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); + + // synchronous versions + static AppLocale setLocaleSync(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocaleSync(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRawSync(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRawSync(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static void setPluralResolverSync({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolverSync( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); } /// Provides utility functions without any side effects. -class AppLocaleUtils extends BaseAppLocaleUtils { - AppLocaleUtils._() - : super(baseLocale: _baseLocale, locales: AppLocale.values); - - static final instance = AppLocaleUtils._(); - - // static aliases (checkout base methods for documentation) - static AppLocale parse(String rawLocale) => instance.parse(rawLocale); - static AppLocale parseLocaleParts( - {required String languageCode, - String? scriptCode, - String? countryCode}) => - instance.parseLocaleParts( - languageCode: languageCode, - scriptCode: scriptCode, - countryCode: countryCode); - static List get supportedLocalesRaw => instance.supportedLocalesRaw; -} - -// translations - -// Path: -class StringsEn implements BaseTranslations { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - StringsEn.build( - {Map? overrides, - PluralResolver? cardinalResolver, - PluralResolver? ordinalResolver}) - : assert(overrides == null, - 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.en, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ) { - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override - final TranslationMetadata $meta; - - /// Access flat map - dynamic operator [](String key) => $meta.getTranslation(key); - - late final StringsEn _root = this; // ignore: unused_field - - // Translations - late final StringsCommandsEn commands = StringsCommandsEn._(_root); - late final StringsServicesEn services = StringsServicesEn._(_root); - late final StringsErrorHandlerEn errorHandler = - StringsErrorHandlerEn._(_root); -} - -// Path: commands -class StringsCommandsEn { - StringsCommandsEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - late final StringsCommandsInfoEn info = StringsCommandsInfoEn._(_root); - late final StringsCommandsSkipEn skip = StringsCommandsSkipEn._(_root); - late final StringsCommandsStopEn stop = StringsCommandsStopEn._(_root); - late final StringsCommandsLeaveEn leave = StringsCommandsLeaveEn._(_root); - late final StringsCommandsJoinEn join = StringsCommandsJoinEn._(_root); - late final StringsCommandsVolumeEn volume = StringsCommandsVolumeEn._(_root); - late final StringsCommandsPauseEn pause = StringsCommandsPauseEn._(_root); - late final StringsCommandsResumeEn resume = StringsCommandsResumeEn._(_root); - late final StringsCommandsMusicEn music = StringsCommandsMusicEn._(_root); - late final StringsCommandsRadioEn radio = StringsCommandsRadioEn._(_root); -} - -// Path: services -class StringsServicesEn { - StringsServicesEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - late final StringsServicesMusicEn music = StringsServicesMusicEn._(_root); -} - -// Path: errorHandler -class StringsErrorHandlerEn { - StringsErrorHandlerEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get title => 'An error has occurred'; - String get fallbackDescription => - 'Your command couldn\'t be executed because of an error. Please contact a developer for more information.'; - String get musicConnectedToVC => - 'I have to be in a voice channel to use this command'; - String get musicNotConnectedToVC => - 'I\'m already connected to a voice channel'; - String get musicSameVC => 'I\'m already being used on other voice channel'; - String get musicUserConnectedToVC => - 'You need to be connected to a voice channel to use this command'; - late final StringsErrorHandlerCooldownEn cooldown = - StringsErrorHandlerCooldownEn._(_root); - late final StringsErrorHandlerUnauthorizedCommandEn unauthorizedCommand = - StringsErrorHandlerUnauthorizedCommandEn._(_root); - late final StringsErrorHandlerMissingArgumentsEn missingArguments = - StringsErrorHandlerMissingArgumentsEn._(_root); - late final StringsErrorHandlerInputParsingFailureEn inputParsingFailure = - StringsErrorHandlerInputParsingFailureEn._(_root); -} - -// Path: commands.info -class StringsCommandsInfoEn { - StringsCommandsInfoEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'info'; - String get description => 'Show information about the current project'; - String get addToServer => 'Add Radio Horizon to your server'; - String shardOf({required Object index, required Object count}) => - 'Shard ${index} of ${count}'; - String get cachedGuilds => 'Cached guilds'; - String get cachedUsers => 'Cached users'; - String get cachedChannels => 'Cached channels'; - String get cachedVoiceStates => 'Cached voice states'; - String get shardCount => 'Shard count'; - String get cachedMessages => 'Cached messages'; - String get memoryUsage => 'Memory usage (current/RSS)'; - String get uptime => 'Uptime'; - String get currentPlayers => 'Current players'; - String get gatewayLatency => 'Gateway latency'; -} - -// Path: commands.skip -class StringsCommandsSkipEn { - StringsCommandsSkipEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'skip'; - String get description => 'Skips the current song'; - String get skipped => 'Skipped current track'; - String get nothingPlaying => 'The queue is clear!'; -} - -// Path: commands.stop -class StringsCommandsStopEn { - StringsCommandsStopEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'stop'; - String get description => - 'Stops the current player and clears its track queue'; - String get stopped => 'Player stopped'; -} - -// Path: commands.leave -class StringsCommandsLeaveEn { - StringsCommandsLeaveEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'leave'; - String get description => 'Leaves the current voice channel'; - String get left => 'Left voice channel'; - String get leftDueToInactivity => 'Left voice channel due to inactivity'; -} - -// Path: commands.join -class StringsCommandsJoinEn { - StringsCommandsJoinEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'join'; - String get description => 'Joins the voice channel you are in'; - String get joined => 'Joined voice channel'; -} - -// Path: commands.volume -class StringsCommandsVolumeEn { - StringsCommandsVolumeEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'volume'; - String get description => 'Sets the volume of the current player'; - String get volumeDescription => - 'The volume to set, this value must be contained between 0 and 1000'; - String volumeSet({required Object volume}) => 'Volume set to ${volume}'; -} - -// Path: commands.pause -class StringsCommandsPauseEn { - StringsCommandsPauseEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'pause'; - String get description => 'Pauses the current player'; - String get paused => 'Player paused'; -} - -// Path: commands.resume -class StringsCommandsResumeEn { - StringsCommandsResumeEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'resume'; - String get description => 'Resumes the current player'; - String get resumed => 'Player resumed'; -} - -// Path: commands.music -class StringsCommandsMusicEn { - StringsCommandsMusicEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'music'; - String get description => 'Music related commands'; - late final StringsCommandsMusicChildrenEn children = - StringsCommandsMusicChildrenEn._(_root); -} - -// Path: commands.radio -class StringsCommandsRadioEn { - StringsCommandsRadioEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'radio'; - String get description => 'Radio related commands'; - late final StringsCommandsRadioChildrenEn children = - StringsCommandsRadioChildrenEn._(_root); -} - -// Path: services.music -class StringsServicesMusicEn { - StringsServicesMusicEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - late final StringsServicesMusicTrackStuckEn trackStuck = - StringsServicesMusicTrackStuckEn._(_root); - late final StringsServicesMusicTrackStartedEn trackStarted = - StringsServicesMusicTrackStartedEn._(_root); - late final StringsServicesMusicTrackExceptionEn trackException = - StringsServicesMusicTrackExceptionEn._(_root); -} - -// Path: errorHandler.cooldown -class StringsErrorHandlerCooldownEn { - StringsErrorHandlerCooldownEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get title => 'Command on cooldown'; - String description({required Object inSeconds}) => - 'You can\'t use this command right now because it is on cooldown. Please wait ${inSeconds}s and try again.'; -} - -// Path: errorHandler.unauthorizedCommand -class StringsErrorHandlerUnauthorizedCommandEn { - StringsErrorHandlerUnauthorizedCommandEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get title => 'You can\'t use this command!'; - String get description => - 'This command can only be used by certain users in certain contexts. Check that you have permission to execute the command, or contact a developer for more information.'; -} - -// Path: errorHandler.missingArguments -class StringsErrorHandlerMissingArgumentsEn { - StringsErrorHandlerMissingArgumentsEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get title => 'Not enough arguments'; - String get description => - 'You didn\'t provide enough arguments for this command. Please try again and use the Slash Command menu for help, or contact a developer for more information.'; -} - -// Path: errorHandler.inputParsingFailure -class StringsErrorHandlerInputParsingFailureEn { - StringsErrorHandlerInputParsingFailureEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get title => 'Couldn\'t parse input'; - String get description => - 'Your command couldn\'t be executed because we were unable to understand your input. Please try again with different inputs or contact a developer for more information.'; -} - -// Path: commands.music.children -class StringsCommandsMusicChildrenEn { - StringsCommandsMusicChildrenEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - late final StringsCommandsMusicChildrenPlayEn play = - StringsCommandsMusicChildrenPlayEn._(_root); -} - -// Path: commands.radio.children -class StringsCommandsRadioChildrenEn { - StringsCommandsRadioChildrenEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - late final StringsCommandsRadioChildrenPlayEn play = - StringsCommandsRadioChildrenPlayEn._(_root); - late final StringsCommandsRadioChildrenRecognizeEn recognize = - StringsCommandsRadioChildrenRecognizeEn._(_root); - late final StringsCommandsRadioChildrenUpvoteEn upvote = - StringsCommandsRadioChildrenUpvoteEn._(_root); - late final StringsCommandsRadioChildrenPlayRandomEn playRandom = - StringsCommandsRadioChildrenPlayRandomEn._(_root); -} - -// Path: services.music.trackStuck -class StringsServicesMusicTrackStuckEn { - StringsServicesMusicTrackStuckEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get title => 'Track stuck'; - String description( - {required Object track, - required Object uri, - required Object requester}) => - 'Track [${track}](${uri}}) stuck playing.\n\nRequested by <@${requester}>'; -} - -// Path: services.music.trackStarted -class StringsServicesMusicTrackStartedEn { - StringsServicesMusicTrackStartedEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get title => 'Track started'; - String description( - {required Object track, - required Object uri, - required Object requester}) => - 'Track [${track}](${uri}}) started playing.\n\nRequested by <@${requester}>'; -} - -// Path: services.music.trackException -class StringsServicesMusicTrackExceptionEn { - StringsServicesMusicTrackExceptionEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get title => 'Track exception'; - String description( - {required Object track, - required Object uri, - required Object requester}) => - 'Track [${track}](${uri}}) threw an exception.\n\nRequested by <@${requester}>'; -} - -// Path: commands.music.children.play -class StringsCommandsMusicChildrenPlayEn { - StringsCommandsMusicChildrenPlayEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'play'; - String get description => 'Plays music based on the given query'; - String get queryDescription => 'The name/url of the song/playlist to play'; - String noResults({required Object query}) => 'No results found for ${query}'; - String playlistEnqueued({required Object name, required Object query}) => - 'Playlist ${name} (${query}) enqueued'; - String songEnqueued({required Object title, required Object query}) => - 'Song ${title} (${query}) enqueued'; -} - -// Path: commands.radio.children.play -class StringsCommandsRadioChildrenPlayEn { - StringsCommandsRadioChildrenPlayEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'play'; - String get description => 'Plays a radio station based on the given query'; - String get queryDescription => 'The name of the radio station to play'; - String searching({required Object query}) => - 'Searching for radio ${query}...'; - String noResults({required Object query}) => 'No results found for ${query}'; - String get startedPlaying => 'Started playing'; - String startedPlayingDescription( - {required Object radio, required Object mention}) => - 'Radio ${radio} started playing.\n\nRequested by ${mention}'; - String stationEnqueued({required Object name, required Object query}) => - 'Station ${name} (${query}) enqueued'; -} - -// Path: commands.radio.children.recognize -class StringsCommandsRadioChildrenRecognizeEn { - StringsCommandsRadioChildrenRecognizeEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'recognize'; - String get description => 'Recognizes the current song playing'; - String requestedBy({required Object mention}) => 'Requested by ${mention}'; - String get radioStationField => 'Radio Station'; - String get genreField => 'Genre'; - String get computationalTimeField => 'Computational time'; - late final StringsCommandsRadioChildrenRecognizeErrorsEn errors = - StringsCommandsRadioChildrenRecognizeErrorsEn._(_root); -} - -// Path: commands.radio.children.upvote -class StringsCommandsRadioChildrenUpvoteEn { - StringsCommandsRadioChildrenUpvoteEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'upvote'; - String get description => 'Upvotes the current radio playing'; - String requestedBy({required Object mention}) => 'Requested by ${mention}'; - String get success => 'Voted successfully'; - String successDescription({required Object radio}) => - 'You have successfully voted for the radio ${radio}! Thank you for your support :D'; - late final StringsCommandsRadioChildrenUpvoteErrorsEn errors = - StringsCommandsRadioChildrenUpvoteErrorsEn._(_root); -} - -// Path: commands.radio.children.playRandom -class StringsCommandsRadioChildrenPlayRandomEn { - StringsCommandsRadioChildrenPlayRandomEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get command => 'play-random'; - String get description => 'Plays a random radio station'; - String get searching => 'Searching for a random radio station...'; - String get startedPlaying => 'Started playing'; - String startedPlayingDescription( - {required Object radio, required Object mention}) => - 'Radio ${radio} started playing.\n\nRequested by ${mention}'; - late final StringsCommandsRadioChildrenPlayRandomErrorsEn errors = - StringsCommandsRadioChildrenPlayRandomErrorsEn._(_root); -} - -// Path: commands.radio.children.recognize.errors -class StringsCommandsRadioChildrenRecognizeErrorsEn { - StringsCommandsRadioChildrenRecognizeErrorsEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get title => 'An error occurred while recognizing the song'; - String get noRadioPlaying => 'Couldn\'t find a radio playing!'; - String get radioCantCommunicate => - 'There was an error communicating with the server, please try again.'; - String get noResults => 'Couldn\'t identify the current song playing :('; -} - -// Path: commands.radio.children.upvote.errors -class StringsCommandsRadioChildrenUpvoteErrorsEn { - StringsCommandsRadioChildrenUpvoteErrorsEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get noRadioPlaying => 'Couldn\'t find a radio playing!'; -} - -// Path: commands.radio.children.playRandom.errors -class StringsCommandsRadioChildrenPlayRandomErrorsEn { - StringsCommandsRadioChildrenPlayRandomErrorsEn._(this._root); - - final StringsEn _root; // ignore: unused_field - - // Translations - String get noResults => - 'Couldn\'t find a random radio station :( Try again later!'; -} - -// Path: -class StringsEs extends StringsEn { - /// You can call this constructor and build your own translation instance of this locale. - /// Constructing via the enum [AppLocale.build] is preferred. - StringsEs.build( - {Map? overrides, - PluralResolver? cardinalResolver, - PluralResolver? ordinalResolver}) - : assert(overrides == null, - 'Set "translation_overrides: true" in order to enable this feature.'), - $meta = TranslationMetadata( - locale: AppLocale.es, - overrides: overrides ?? {}, - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver, - ), - super.build( - cardinalResolver: cardinalResolver, - ordinalResolver: ordinalResolver) { - super.$meta.setFlatMapFunction( - $meta.getTranslation); // copy base translations to super.$meta - $meta.setFlatMapFunction(_flatMapFunction); - } - - /// Metadata for the translations of . - @override - final TranslationMetadata $meta; - - /// Access flat map - @override - dynamic operator [](String key) => - $meta.getTranslation(key) ?? super.$meta.getTranslation(key); - - @override - late final StringsEs _root = this; // ignore: unused_field - - // Translations - @override - late final StringsCommandsEs commands = StringsCommandsEs._(_root); - @override - late final StringsServicesEs services = StringsServicesEs._(_root); - @override - late final StringsErrorHandlerEs errorHandler = - StringsErrorHandlerEs._(_root); -} - -// Path: commands -class StringsCommandsEs extends StringsCommandsEn { - StringsCommandsEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - late final StringsCommandsInfoEs info = StringsCommandsInfoEs._(_root); - @override - late final StringsCommandsSkipEs skip = StringsCommandsSkipEs._(_root); - @override - late final StringsCommandsStopEs stop = StringsCommandsStopEs._(_root); - @override - late final StringsCommandsLeaveEs leave = StringsCommandsLeaveEs._(_root); - @override - late final StringsCommandsJoinEs join = StringsCommandsJoinEs._(_root); - @override - late final StringsCommandsVolumeEs volume = StringsCommandsVolumeEs._(_root); - @override - late final StringsCommandsPauseEs pause = StringsCommandsPauseEs._(_root); - @override - late final StringsCommandsResumeEs resume = StringsCommandsResumeEs._(_root); - @override - late final StringsCommandsMusicEs music = StringsCommandsMusicEs._(_root); - @override - late final StringsCommandsRadioEs radio = StringsCommandsRadioEs._(_root); -} - -// Path: services -class StringsServicesEs extends StringsServicesEn { - StringsServicesEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - late final StringsServicesMusicEs music = StringsServicesMusicEs._(_root); -} - -// Path: errorHandler -class StringsErrorHandlerEs extends StringsErrorHandlerEn { - StringsErrorHandlerEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get title => 'Ha ocurrido un error'; - @override - String get fallbackDescription => - 'Tu comando no se ha podido ejecutar debido a un error desconocido. Por favor contacte a un desarrollador para más información.'; - @override - String get musicConnectedToVC => - 'Debo estar conectado a un canal de voz para ejecutar este comando'; - @override - String get musicNotConnectedToVC => 'Ya estoy conectado a un canal de voz'; - @override - String get musicSameVC => - 'Debes estar en el mismo canal de voz que yo para ejecutar este comando'; - @override - String get musicUserConnectedToVC => - 'Debes estar conectado a un canal de voz para ejecutar este comando'; - @override - late final StringsErrorHandlerCooldownEs cooldown = - StringsErrorHandlerCooldownEs._(_root); - @override - late final StringsErrorHandlerUnauthorizedCommandEs unauthorizedCommand = - StringsErrorHandlerUnauthorizedCommandEs._(_root); - @override - late final StringsErrorHandlerMissingArgumentsEs missingArguments = - StringsErrorHandlerMissingArgumentsEs._(_root); - @override - late final StringsErrorHandlerInputParsingFailureEs inputParsingFailure = - StringsErrorHandlerInputParsingFailureEs._(_root); -} - -// Path: commands.info -class StringsCommandsInfoEs extends StringsCommandsInfoEn { - StringsCommandsInfoEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'info'; - @override - String get description => 'Muestra información sobre el bot'; - @override - String get addToServer => 'Añadir Radio Horizon a mi servidor'; - @override - String shardOf({required Object index, required Object count}) => - 'Fragmento ${index} de ${count}'; - @override - String get cachedGuilds => 'Servidores en caché'; - @override - String get cachedUsers => 'Usuarios en caché'; - @override - String get cachedChannels => 'Canales en caché'; - @override - String get cachedVoiceStates => 'Estados de voz en caché'; - @override - String get shardCount => 'Fragmentos'; - @override - String get cachedMessages => 'Mensajes en caché'; - @override - String get memoryUsage => 'Uso de memoria (current/RSS)'; - @override - String get uptime => 'Tiempo de actividad'; - @override - String get currentPlayers => 'Reproductores actuales'; - @override - String get gatewayLatency => 'Latencia de la puerta de enlace'; -} - -// Path: commands.skip -class StringsCommandsSkipEs extends StringsCommandsSkipEn { - StringsCommandsSkipEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'saltar'; - @override - String get description => 'Salta a la siguiente canción en la cola'; - @override - String get skipped => 'La canción actual ha sido saltada'; - @override - String get nothingPlaying => 'La cola está vacía'; -} - -// Path: commands.stop -class StringsCommandsStopEs extends StringsCommandsStopEn { - StringsCommandsStopEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'parar'; - @override - String get description => 'Para la reproducción de música'; - @override - String get stopped => 'La reproducción de música ha sido detenida'; -} - -// Path: commands.leave -class StringsCommandsLeaveEs extends StringsCommandsLeaveEn { - StringsCommandsLeaveEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'dejar'; - @override - String get description => 'Abandona el canal de voz'; - @override - String get left => 'El canal de voz ha sido abandonado'; - @override - String get leftDueToInactivity => - 'El canal de voz ha sido abandonado debido a la inactividad'; -} - -// Path: commands.join -class StringsCommandsJoinEs extends StringsCommandsJoinEn { - StringsCommandsJoinEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'unirse'; - @override - String get description => 'Se une al canal de voz en el que estás'; - @override - String get joined => 'Se ha unido al canal de voz'; -} - -// Path: commands.volume -class StringsCommandsVolumeEs extends StringsCommandsVolumeEn { - StringsCommandsVolumeEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'volumen'; - @override - String get description => 'Establece el volumen de la música'; - @override - String get volumeDescription => - 'El volumen para establecer, debe ser un valor entre 0 y 1000'; - @override - String volumeSet({required Object volume}) => 'Volumen puesto en ${volume}'; -} - -// Path: commands.pause -class StringsCommandsPauseEs extends StringsCommandsPauseEn { - StringsCommandsPauseEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'pausar'; - @override - String get description => 'Pausa la reproducción de música'; - @override - String get paused => 'La reproducción de música ha sido pausada'; -} - -// Path: commands.resume -class StringsCommandsResumeEs extends StringsCommandsResumeEn { - StringsCommandsResumeEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'reanudar'; - @override - String get description => 'Reanuda la reproducción de música'; - @override - String get resumed => 'La reproducción de música ha sido reanudada'; -} - -// Path: commands.music -class StringsCommandsMusicEs extends StringsCommandsMusicEn { - StringsCommandsMusicEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'musica'; - @override - String get description => - 'Comandos relacionados con la funcionalidad de música'; - @override - late final StringsCommandsMusicChildrenEs children = - StringsCommandsMusicChildrenEs._(_root); -} - -// Path: commands.radio -class StringsCommandsRadioEs extends StringsCommandsRadioEn { - StringsCommandsRadioEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'radio'; - @override - String get description => - 'Comandos relacionados con la funcionalidad de radio'; - @override - late final StringsCommandsRadioChildrenEs children = - StringsCommandsRadioChildrenEs._(_root); -} - -// Path: services.music -class StringsServicesMusicEs extends StringsServicesMusicEn { - StringsServicesMusicEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - late final StringsServicesMusicTrackStuckEs trackStuck = - StringsServicesMusicTrackStuckEs._(_root); - @override - late final StringsServicesMusicTrackStartedEs trackStarted = - StringsServicesMusicTrackStartedEs._(_root); - @override - late final StringsServicesMusicTrackExceptionEs trackException = - StringsServicesMusicTrackExceptionEs._(_root); -} - -// Path: errorHandler.cooldown -class StringsErrorHandlerCooldownEs extends StringsErrorHandlerCooldownEn { - StringsErrorHandlerCooldownEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get title => 'Commando en cooldown'; - @override - String description({required Object inSeconds}) => - 'No puedes usar este comando ahora mismo porque está en cooldown. Por favor espera ${inSeconds}s e inténtalo de nuevo.'; -} - -// Path: errorHandler.unauthorizedCommand -class StringsErrorHandlerUnauthorizedCommandEs - extends StringsErrorHandlerUnauthorizedCommandEn { - StringsErrorHandlerUnauthorizedCommandEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get title => 'No puedes usar este comando!'; - @override - String get description => - 'Este comando solo puede ser usado por determinados usuarios en determinados contextos. Verifica que tienes los permisos para ejecutar este comando, o contacte a un desarrollador para más información.'; -} - -// Path: errorHandler.missingArguments -class StringsErrorHandlerMissingArgumentsEs - extends StringsErrorHandlerMissingArgumentsEn { - StringsErrorHandlerMissingArgumentsEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get title => 'Faltan argumentos'; - @override - String get description => - 'No has proveido los argumentos necesarios para ejecutar esta función. Por favor inténtalo de nuevo y usa el Menú de Slash Command para ayuda, o contacta a un desarrollador para más información.'; -} - -// Path: errorHandler.inputParsingFailure -class StringsErrorHandlerInputParsingFailureEs - extends StringsErrorHandlerInputParsingFailureEn { - StringsErrorHandlerInputParsingFailureEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get title => 'Ha ocurrido un error al procesar tu entrada'; - @override - String get description => - 'No se ha podido ejecutar el comando ya que no hemos podido interpretar tus argumentos. Por favor inténtalo de nuevo y usa el Menú de Slash Command para ayuda, o contacta a un desarrollador para más información.'; -} - -// Path: commands.music.children -class StringsCommandsMusicChildrenEs extends StringsCommandsMusicChildrenEn { - StringsCommandsMusicChildrenEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - late final StringsCommandsMusicChildrenPlayEs play = - StringsCommandsMusicChildrenPlayEs._(_root); -} - -// Path: commands.radio.children -class StringsCommandsRadioChildrenEs extends StringsCommandsRadioChildrenEn { - StringsCommandsRadioChildrenEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - late final StringsCommandsRadioChildrenPlayEs play = - StringsCommandsRadioChildrenPlayEs._(_root); - @override - late final StringsCommandsRadioChildrenRecognizeEs recognize = - StringsCommandsRadioChildrenRecognizeEs._(_root); - @override - late final StringsCommandsRadioChildrenUpvoteEs upvote = - StringsCommandsRadioChildrenUpvoteEs._(_root); - @override - late final StringsCommandsRadioChildrenPlayRandomEs playRandom = - StringsCommandsRadioChildrenPlayRandomEs._(_root); -} - -// Path: services.music.trackStuck -class StringsServicesMusicTrackStuckEs - extends StringsServicesMusicTrackStuckEn { - StringsServicesMusicTrackStuckEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get title => 'La canción se ha quedado atascada'; - @override - String description( - {required Object track, - required Object uri, - required Object requester}) => - 'La pista [${track}](${uri}}) se ha quedado atascada.\n\nPedida por <@${requester}>'; -} - -// Path: services.music.trackStarted -class StringsServicesMusicTrackStartedEs - extends StringsServicesMusicTrackStartedEn { - StringsServicesMusicTrackStartedEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get title => 'Se ha comenzado a reproducir'; - @override - String description( - {required Object track, - required Object uri, - required Object requester}) => - 'La pista [${track}](${uri}}) se ha comenzado a reproducir.\n\nPedido por <@${requester}>'; -} - -// Path: services.music.trackException -class StringsServicesMusicTrackExceptionEs - extends StringsServicesMusicTrackExceptionEn { - StringsServicesMusicTrackExceptionEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get title => 'Ha ocurrido un error al reproducir la canción'; - @override - String description( - {required Object track, - required Object uri, - required Object requester}) => - 'La pista [${track}](${uri}}) devolvió un error.\n\nPedido por <@${requester}>'; -} - -// Path: commands.music.children.play -class StringsCommandsMusicChildrenPlayEs - extends StringsCommandsMusicChildrenPlayEn { - StringsCommandsMusicChildrenPlayEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'reproducir'; - @override - String get description => 'Reproduce música basada en una URL o una búsqueda'; - @override - String get queryDescription => 'El nombre de una canción o una URL'; - @override - String noResults({required Object query}) => - 'No se encontraron resultados para ${query}'; - @override - String playlistEnqueued({required Object name, required Object query}) => - 'Lista de reproducción ${name} (${query}) añadida a la cola'; - @override - String songEnqueued({required Object title, required Object query}) => - 'Canción ${title} (${query}) añadida a la cola'; -} - -// Path: commands.radio.children.play -class StringsCommandsRadioChildrenPlayEs - extends StringsCommandsRadioChildrenPlayEn { - StringsCommandsRadioChildrenPlayEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'reproducir'; - @override - String get description => 'Reproduce una radio basada en una búsqueda'; - @override - String get queryDescription => 'El nombre de una estación de radio'; - @override - String searching({required Object query}) => 'Buscando radio ${query}...'; - @override - String noResults({required Object query}) => - 'No se encontraron resultados para ${query}'; - @override - String get startedPlaying => 'Se ha comenzado a reproducir'; - @override - String startedPlayingDescription( - {required Object radio, required Object mention}) => - 'La radio ${radio} ha comenzado a reproducirse.\n\nPedido por ${mention}'; - @override - String stationEnqueued({required Object name, required Object query}) => - 'La radio ${name} (${query}) ha sido añadida a la cola'; -} - -// Path: commands.radio.children.recognize -class StringsCommandsRadioChildrenRecognizeEs - extends StringsCommandsRadioChildrenRecognizeEn { - StringsCommandsRadioChildrenRecognizeEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'reconocer'; - @override - String get description => - 'Reconoce la cancion que se está reproduciendo en la radio'; - @override - String requestedBy({required Object mention}) => 'Pedido por ${mention}'; - @override - String get radioStationField => 'Estación'; - @override - String get genreField => 'Género'; - @override - String get computationalTimeField => 'Tiempo de cálculo'; - @override - late final StringsCommandsRadioChildrenRecognizeErrorsEs errors = - StringsCommandsRadioChildrenRecognizeErrorsEs._(_root); -} - -// Path: commands.radio.children.upvote -class StringsCommandsRadioChildrenUpvoteEs - extends StringsCommandsRadioChildrenUpvoteEn { - StringsCommandsRadioChildrenUpvoteEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'votar'; - @override - String get description => - 'Vota positivamente por la radio que se está reproduciendo'; - @override - String requestedBy({required Object mention}) => 'Pedido por ${mention}'; - @override - String get success => 'Voto positivo añadido'; - @override - String successDescription({required Object radio}) => - 'Has votado positivamente por la radio ${radio}! Gracias por tu apoyo :D'; - @override - late final StringsCommandsRadioChildrenUpvoteErrorsEs errors = - StringsCommandsRadioChildrenUpvoteErrorsEs._(_root); -} - -// Path: commands.radio.children.playRandom -class StringsCommandsRadioChildrenPlayRandomEs - extends StringsCommandsRadioChildrenPlayRandomEn { - StringsCommandsRadioChildrenPlayRandomEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get command => 'play-random'; - @override - String get description => 'Reproduce una radio aleatoria'; - @override - String get searching => 'Buscando una radio aleatoria...'; - @override - String get startedPlaying => 'Se ha comenzado a reproducir'; - @override - String startedPlayingDescription( - {required Object radio, required Object mention}) => - 'La radio ${radio} ha comenzado a reproducirse.\n\nPedido por ${mention}'; - @override - late final StringsCommandsRadioChildrenPlayRandomErrorsEs errors = - StringsCommandsRadioChildrenPlayRandomErrorsEs._(_root); -} - -// Path: commands.radio.children.recognize.errors -class StringsCommandsRadioChildrenRecognizeErrorsEs - extends StringsCommandsRadioChildrenRecognizeErrorsEn { - StringsCommandsRadioChildrenRecognizeErrorsEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get title => 'Ha ocurrido un error al reconocer la canción'; - @override - String get noRadioPlaying => 'No se está reproduciendo ninguna radio'; - @override - String get radioCantCommunicate => - 'La radio no puede comunicarse con el servidor de reconocimiento de canciones. Inténtalo de nuevo más tarde'; - @override - String get noResults => - 'No se han encontrado resultados para la canción que se está reproduciendo :('; -} - -// Path: commands.radio.children.upvote.errors -class StringsCommandsRadioChildrenUpvoteErrorsEs - extends StringsCommandsRadioChildrenUpvoteErrorsEn { - StringsCommandsRadioChildrenUpvoteErrorsEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get noRadioPlaying => 'No se está reproduciendo ninguna radio'; -} - -// Path: commands.radio.children.playRandom.errors -class StringsCommandsRadioChildrenPlayRandomErrorsEs - extends StringsCommandsRadioChildrenPlayRandomErrorsEn { - StringsCommandsRadioChildrenPlayRandomErrorsEs._(StringsEs root) - : this._root = root, - super._(root); - - @override - final StringsEs _root; // ignore: unused_field - - // Translations - @override - String get noResults => - 'No se ha podido encontrar una radio aleatoria :( Inténtalo de nuevo más tarde!'; -} - -/// Flat map(s) containing all translations. -/// Only for edge cases! For simple maps, use the map function of this library. - -extension on StringsEn { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'commands.info.command': - return 'info'; - case 'commands.info.description': - return 'Show information about the current project'; - case 'commands.info.addToServer': - return 'Add Radio Horizon to your server'; - case 'commands.info.shardOf': - return ({required Object index, required Object count}) => - 'Shard ${index} of ${count}'; - case 'commands.info.cachedGuilds': - return 'Cached guilds'; - case 'commands.info.cachedUsers': - return 'Cached users'; - case 'commands.info.cachedChannels': - return 'Cached channels'; - case 'commands.info.cachedVoiceStates': - return 'Cached voice states'; - case 'commands.info.shardCount': - return 'Shard count'; - case 'commands.info.cachedMessages': - return 'Cached messages'; - case 'commands.info.memoryUsage': - return 'Memory usage (current/RSS)'; - case 'commands.info.uptime': - return 'Uptime'; - case 'commands.info.currentPlayers': - return 'Current players'; - case 'commands.info.gatewayLatency': - return 'Gateway latency'; - case 'commands.skip.command': - return 'skip'; - case 'commands.skip.description': - return 'Skips the current song'; - case 'commands.skip.skipped': - return 'Skipped current track'; - case 'commands.skip.nothingPlaying': - return 'The queue is clear!'; - case 'commands.stop.command': - return 'stop'; - case 'commands.stop.description': - return 'Stops the current player and clears its track queue'; - case 'commands.stop.stopped': - return 'Player stopped'; - case 'commands.leave.command': - return 'leave'; - case 'commands.leave.description': - return 'Leaves the current voice channel'; - case 'commands.leave.left': - return 'Left voice channel'; - case 'commands.leave.leftDueToInactivity': - return 'Left voice channel due to inactivity'; - case 'commands.join.command': - return 'join'; - case 'commands.join.description': - return 'Joins the voice channel you are in'; - case 'commands.join.joined': - return 'Joined voice channel'; - case 'commands.volume.command': - return 'volume'; - case 'commands.volume.description': - return 'Sets the volume of the current player'; - case 'commands.volume.volumeDescription': - return 'The volume to set, this value must be contained between 0 and 1000'; - case 'commands.volume.volumeSet': - return ({required Object volume}) => 'Volume set to ${volume}'; - case 'commands.pause.command': - return 'pause'; - case 'commands.pause.description': - return 'Pauses the current player'; - case 'commands.pause.paused': - return 'Player paused'; - case 'commands.resume.command': - return 'resume'; - case 'commands.resume.description': - return 'Resumes the current player'; - case 'commands.resume.resumed': - return 'Player resumed'; - case 'commands.music.command': - return 'music'; - case 'commands.music.description': - return 'Music related commands'; - case 'commands.music.children.play.command': - return 'play'; - case 'commands.music.children.play.description': - return 'Plays music based on the given query'; - case 'commands.music.children.play.queryDescription': - return 'The name/url of the song/playlist to play'; - case 'commands.music.children.play.noResults': - return ({required Object query}) => 'No results found for ${query}'; - case 'commands.music.children.play.playlistEnqueued': - return ({required Object name, required Object query}) => - 'Playlist ${name} (${query}) enqueued'; - case 'commands.music.children.play.songEnqueued': - return ({required Object title, required Object query}) => - 'Song ${title} (${query}) enqueued'; - case 'commands.radio.command': - return 'radio'; - case 'commands.radio.description': - return 'Radio related commands'; - case 'commands.radio.children.play.command': - return 'play'; - case 'commands.radio.children.play.description': - return 'Plays a radio station based on the given query'; - case 'commands.radio.children.play.queryDescription': - return 'The name of the radio station to play'; - case 'commands.radio.children.play.searching': - return ({required Object query}) => 'Searching for radio ${query}...'; - case 'commands.radio.children.play.noResults': - return ({required Object query}) => 'No results found for ${query}'; - case 'commands.radio.children.play.startedPlaying': - return 'Started playing'; - case 'commands.radio.children.play.startedPlayingDescription': - return ({required Object radio, required Object mention}) => - 'Radio ${radio} started playing.\n\nRequested by ${mention}'; - case 'commands.radio.children.play.stationEnqueued': - return ({required Object name, required Object query}) => - 'Station ${name} (${query}) enqueued'; - case 'commands.radio.children.recognize.command': - return 'recognize'; - case 'commands.radio.children.recognize.description': - return 'Recognizes the current song playing'; - case 'commands.radio.children.recognize.requestedBy': - return ({required Object mention}) => 'Requested by ${mention}'; - case 'commands.radio.children.recognize.radioStationField': - return 'Radio Station'; - case 'commands.radio.children.recognize.genreField': - return 'Genre'; - case 'commands.radio.children.recognize.computationalTimeField': - return 'Computational time'; - case 'commands.radio.children.recognize.errors.title': - return 'An error occurred while recognizing the song'; - case 'commands.radio.children.recognize.errors.noRadioPlaying': - return 'Couldn\'t find a radio playing!'; - case 'commands.radio.children.recognize.errors.radioCantCommunicate': - return 'There was an error communicating with the server, please try again.'; - case 'commands.radio.children.recognize.errors.noResults': - return 'Couldn\'t identify the current song playing :('; - case 'commands.radio.children.upvote.command': - return 'upvote'; - case 'commands.radio.children.upvote.description': - return 'Upvotes the current radio playing'; - case 'commands.radio.children.upvote.requestedBy': - return ({required Object mention}) => 'Requested by ${mention}'; - case 'commands.radio.children.upvote.success': - return 'Voted successfully'; - case 'commands.radio.children.upvote.successDescription': - return ({required Object radio}) => - 'You have successfully voted for the radio ${radio}! Thank you for your support :D'; - case 'commands.radio.children.upvote.errors.noRadioPlaying': - return 'Couldn\'t find a radio playing!'; - case 'commands.radio.children.playRandom.command': - return 'play-random'; - case 'commands.radio.children.playRandom.description': - return 'Plays a random radio station'; - case 'commands.radio.children.playRandom.searching': - return 'Searching for a random radio station...'; - case 'commands.radio.children.playRandom.startedPlaying': - return 'Started playing'; - case 'commands.radio.children.playRandom.startedPlayingDescription': - return ({required Object radio, required Object mention}) => - 'Radio ${radio} started playing.\n\nRequested by ${mention}'; - case 'commands.radio.children.playRandom.errors.noResults': - return 'Couldn\'t find a random radio station :( Try again later!'; - case 'services.music.trackStuck.title': - return 'Track stuck'; - case 'services.music.trackStuck.description': - return ( - {required Object track, - required Object uri, - required Object requester}) => - 'Track [${track}](${uri}}) stuck playing.\n\nRequested by <@${requester}>'; - case 'services.music.trackStarted.title': - return 'Track started'; - case 'services.music.trackStarted.description': - return ( - {required Object track, - required Object uri, - required Object requester}) => - 'Track [${track}](${uri}}) started playing.\n\nRequested by <@${requester}>'; - case 'services.music.trackException.title': - return 'Track exception'; - case 'services.music.trackException.description': - return ( - {required Object track, - required Object uri, - required Object requester}) => - 'Track [${track}](${uri}}) threw an exception.\n\nRequested by <@${requester}>'; - case 'errorHandler.title': - return 'An error has occurred'; - case 'errorHandler.fallbackDescription': - return 'Your command couldn\'t be executed because of an error. Please contact a developer for more information.'; - case 'errorHandler.musicConnectedToVC': - return 'I have to be in a voice channel to use this command'; - case 'errorHandler.musicNotConnectedToVC': - return 'I\'m already connected to a voice channel'; - case 'errorHandler.musicSameVC': - return 'I\'m already being used on other voice channel'; - case 'errorHandler.musicUserConnectedToVC': - return 'You need to be connected to a voice channel to use this command'; - case 'errorHandler.cooldown.title': - return 'Command on cooldown'; - case 'errorHandler.cooldown.description': - return ({required Object inSeconds}) => - 'You can\'t use this command right now because it is on cooldown. Please wait ${inSeconds}s and try again.'; - case 'errorHandler.unauthorizedCommand.title': - return 'You can\'t use this command!'; - case 'errorHandler.unauthorizedCommand.description': - return 'This command can only be used by certain users in certain contexts. Check that you have permission to execute the command, or contact a developer for more information.'; - case 'errorHandler.missingArguments.title': - return 'Not enough arguments'; - case 'errorHandler.missingArguments.description': - return 'You didn\'t provide enough arguments for this command. Please try again and use the Slash Command menu for help, or contact a developer for more information.'; - case 'errorHandler.inputParsingFailure.title': - return 'Couldn\'t parse input'; - case 'errorHandler.inputParsingFailure.description': - return 'Your command couldn\'t be executed because we were unable to understand your input. Please try again with different inputs or contact a developer for more information.'; - default: - return null; - } - } -} - -extension on StringsEs { - dynamic _flatMapFunction(String path) { - switch (path) { - case 'commands.info.command': - return 'info'; - case 'commands.info.description': - return 'Muestra información sobre el bot'; - case 'commands.info.addToServer': - return 'Añadir Radio Horizon a mi servidor'; - case 'commands.info.shardOf': - return ({required Object index, required Object count}) => - 'Fragmento ${index} de ${count}'; - case 'commands.info.cachedGuilds': - return 'Servidores en caché'; - case 'commands.info.cachedUsers': - return 'Usuarios en caché'; - case 'commands.info.cachedChannels': - return 'Canales en caché'; - case 'commands.info.cachedVoiceStates': - return 'Estados de voz en caché'; - case 'commands.info.shardCount': - return 'Fragmentos'; - case 'commands.info.cachedMessages': - return 'Mensajes en caché'; - case 'commands.info.memoryUsage': - return 'Uso de memoria (current/RSS)'; - case 'commands.info.uptime': - return 'Tiempo de actividad'; - case 'commands.info.currentPlayers': - return 'Reproductores actuales'; - case 'commands.info.gatewayLatency': - return 'Latencia de la puerta de enlace'; - case 'commands.skip.command': - return 'saltar'; - case 'commands.skip.description': - return 'Salta a la siguiente canción en la cola'; - case 'commands.skip.skipped': - return 'La canción actual ha sido saltada'; - case 'commands.skip.nothingPlaying': - return 'La cola está vacía'; - case 'commands.stop.command': - return 'parar'; - case 'commands.stop.description': - return 'Para la reproducción de música'; - case 'commands.stop.stopped': - return 'La reproducción de música ha sido detenida'; - case 'commands.leave.command': - return 'dejar'; - case 'commands.leave.description': - return 'Abandona el canal de voz'; - case 'commands.leave.left': - return 'El canal de voz ha sido abandonado'; - case 'commands.leave.leftDueToInactivity': - return 'El canal de voz ha sido abandonado debido a la inactividad'; - case 'commands.join.command': - return 'unirse'; - case 'commands.join.description': - return 'Se une al canal de voz en el que estás'; - case 'commands.join.joined': - return 'Se ha unido al canal de voz'; - case 'commands.volume.command': - return 'volumen'; - case 'commands.volume.description': - return 'Establece el volumen de la música'; - case 'commands.volume.volumeDescription': - return 'El volumen para establecer, debe ser un valor entre 0 y 1000'; - case 'commands.volume.volumeSet': - return ({required Object volume}) => 'Volumen puesto en ${volume}'; - case 'commands.pause.command': - return 'pausar'; - case 'commands.pause.description': - return 'Pausa la reproducción de música'; - case 'commands.pause.paused': - return 'La reproducción de música ha sido pausada'; - case 'commands.resume.command': - return 'reanudar'; - case 'commands.resume.description': - return 'Reanuda la reproducción de música'; - case 'commands.resume.resumed': - return 'La reproducción de música ha sido reanudada'; - case 'commands.music.command': - return 'musica'; - case 'commands.music.description': - return 'Comandos relacionados con la funcionalidad de música'; - case 'commands.music.children.play.command': - return 'reproducir'; - case 'commands.music.children.play.description': - return 'Reproduce música basada en una URL o una búsqueda'; - case 'commands.music.children.play.queryDescription': - return 'El nombre de una canción o una URL'; - case 'commands.music.children.play.noResults': - return ({required Object query}) => - 'No se encontraron resultados para ${query}'; - case 'commands.music.children.play.playlistEnqueued': - return ({required Object name, required Object query}) => - 'Lista de reproducción ${name} (${query}) añadida a la cola'; - case 'commands.music.children.play.songEnqueued': - return ({required Object title, required Object query}) => - 'Canción ${title} (${query}) añadida a la cola'; - case 'commands.radio.command': - return 'radio'; - case 'commands.radio.description': - return 'Comandos relacionados con la funcionalidad de radio'; - case 'commands.radio.children.play.command': - return 'reproducir'; - case 'commands.radio.children.play.description': - return 'Reproduce una radio basada en una búsqueda'; - case 'commands.radio.children.play.queryDescription': - return 'El nombre de una estación de radio'; - case 'commands.radio.children.play.searching': - return ({required Object query}) => 'Buscando radio ${query}...'; - case 'commands.radio.children.play.noResults': - return ({required Object query}) => - 'No se encontraron resultados para ${query}'; - case 'commands.radio.children.play.startedPlaying': - return 'Se ha comenzado a reproducir'; - case 'commands.radio.children.play.startedPlayingDescription': - return ({required Object radio, required Object mention}) => - 'La radio ${radio} ha comenzado a reproducirse.\n\nPedido por ${mention}'; - case 'commands.radio.children.play.stationEnqueued': - return ({required Object name, required Object query}) => - 'La radio ${name} (${query}) ha sido añadida a la cola'; - case 'commands.radio.children.recognize.command': - return 'reconocer'; - case 'commands.radio.children.recognize.description': - return 'Reconoce la cancion que se está reproduciendo en la radio'; - case 'commands.radio.children.recognize.requestedBy': - return ({required Object mention}) => 'Pedido por ${mention}'; - case 'commands.radio.children.recognize.radioStationField': - return 'Estación'; - case 'commands.radio.children.recognize.genreField': - return 'Género'; - case 'commands.radio.children.recognize.computationalTimeField': - return 'Tiempo de cálculo'; - case 'commands.radio.children.recognize.errors.title': - return 'Ha ocurrido un error al reconocer la canción'; - case 'commands.radio.children.recognize.errors.noRadioPlaying': - return 'No se está reproduciendo ninguna radio'; - case 'commands.radio.children.recognize.errors.radioCantCommunicate': - return 'La radio no puede comunicarse con el servidor de reconocimiento de canciones. Inténtalo de nuevo más tarde'; - case 'commands.radio.children.recognize.errors.noResults': - return 'No se han encontrado resultados para la canción que se está reproduciendo :('; - case 'commands.radio.children.upvote.command': - return 'votar'; - case 'commands.radio.children.upvote.description': - return 'Vota positivamente por la radio que se está reproduciendo'; - case 'commands.radio.children.upvote.requestedBy': - return ({required Object mention}) => 'Pedido por ${mention}'; - case 'commands.radio.children.upvote.success': - return 'Voto positivo añadido'; - case 'commands.radio.children.upvote.successDescription': - return ({required Object radio}) => - 'Has votado positivamente por la radio ${radio}! Gracias por tu apoyo :D'; - case 'commands.radio.children.upvote.errors.noRadioPlaying': - return 'No se está reproduciendo ninguna radio'; - case 'commands.radio.children.playRandom.command': - return 'play-random'; - case 'commands.radio.children.playRandom.description': - return 'Reproduce una radio aleatoria'; - case 'commands.radio.children.playRandom.searching': - return 'Buscando una radio aleatoria...'; - case 'commands.radio.children.playRandom.startedPlaying': - return 'Se ha comenzado a reproducir'; - case 'commands.radio.children.playRandom.startedPlayingDescription': - return ({required Object radio, required Object mention}) => - 'La radio ${radio} ha comenzado a reproducirse.\n\nPedido por ${mention}'; - case 'commands.radio.children.playRandom.errors.noResults': - return 'No se ha podido encontrar una radio aleatoria :( Inténtalo de nuevo más tarde!'; - case 'services.music.trackStuck.title': - return 'La canción se ha quedado atascada'; - case 'services.music.trackStuck.description': - return ( - {required Object track, - required Object uri, - required Object requester}) => - 'La pista [${track}](${uri}}) se ha quedado atascada.\n\nPedida por <@${requester}>'; - case 'services.music.trackStarted.title': - return 'Se ha comenzado a reproducir'; - case 'services.music.trackStarted.description': - return ( - {required Object track, - required Object uri, - required Object requester}) => - 'La pista [${track}](${uri}}) se ha comenzado a reproducir.\n\nPedido por <@${requester}>'; - case 'services.music.trackException.title': - return 'Ha ocurrido un error al reproducir la canción'; - case 'services.music.trackException.description': - return ( - {required Object track, - required Object uri, - required Object requester}) => - 'La pista [${track}](${uri}}) devolvió un error.\n\nPedido por <@${requester}>'; - case 'errorHandler.title': - return 'Ha ocurrido un error'; - case 'errorHandler.fallbackDescription': - return 'Tu comando no se ha podido ejecutar debido a un error desconocido. Por favor contacte a un desarrollador para más información.'; - case 'errorHandler.musicConnectedToVC': - return 'Debo estar conectado a un canal de voz para ejecutar este comando'; - case 'errorHandler.musicNotConnectedToVC': - return 'Ya estoy conectado a un canal de voz'; - case 'errorHandler.musicSameVC': - return 'Debes estar en el mismo canal de voz que yo para ejecutar este comando'; - case 'errorHandler.musicUserConnectedToVC': - return 'Debes estar conectado a un canal de voz para ejecutar este comando'; - case 'errorHandler.cooldown.title': - return 'Commando en cooldown'; - case 'errorHandler.cooldown.description': - return ({required Object inSeconds}) => - 'No puedes usar este comando ahora mismo porque está en cooldown. Por favor espera ${inSeconds}s e inténtalo de nuevo.'; - case 'errorHandler.unauthorizedCommand.title': - return 'No puedes usar este comando!'; - case 'errorHandler.unauthorizedCommand.description': - return 'Este comando solo puede ser usado por determinados usuarios en determinados contextos. Verifica que tienes los permisos para ejecutar este comando, o contacte a un desarrollador para más información.'; - case 'errorHandler.missingArguments.title': - return 'Faltan argumentos'; - case 'errorHandler.missingArguments.description': - return 'No has proveido los argumentos necesarios para ejecutar esta función. Por favor inténtalo de nuevo y usa el Menú de Slash Command para ayuda, o contacta a un desarrollador para más información.'; - case 'errorHandler.inputParsingFailure.title': - return 'Ha ocurrido un error al procesar tu entrada'; - case 'errorHandler.inputParsingFailure.description': - return 'No se ha podido ejecutar el comando ya que no hemos podido interpretar tus argumentos. Por favor inténtalo de nuevo y usa el Menú de Slash Command para ayuda, o contacta a un desarrollador para más información.'; - default: - return null; - } - } +class AppLocaleUtils extends BaseAppLocaleUtils { + AppLocaleUtils._() : super( + baseLocale: AppLocale.en, + locales: AppLocale.values, + ); + + static final instance = AppLocaleUtils._(); + + // static aliases (checkout base methods for documentation) + static AppLocale parse(String rawLocale) => instance.parse(rawLocale); + static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); + static List get supportedLocalesRaw => instance.supportedLocalesRaw; } diff --git a/lib/i18n/strings.i18n.json b/lib/i18n/strings.i18n.json index 41ceaa0..28e2232 100644 --- a/lib/i18n/strings.i18n.json +++ b/lib/i18n/strings.i18n.json @@ -14,7 +14,8 @@ "memoryUsage": "Memory usage (current/RSS)", "uptime": "Uptime", "currentPlayers": "Current players", - "gatewayLatency": "Gateway latency" + "gatewayLatency": "Gateway latency", + "viewOnGithub": "View on GitHub" }, "skip": { "command": "skip", @@ -64,7 +65,21 @@ "queryDescription": "The name/url of the song/playlist to play", "noResults": "No results found for $query", "playlistEnqueued": "Playlist $name (${query}) enqueued", - "songEnqueued": "Song $title (${query}) enqueued" + "songEnqueued": "Song $title (${query}) enqueued", + "children": { + "youtube": { + "command": "youtube", + "description": "Plays music from YouTube", + "searching": "Searching for $query on YouTube...", + "noResults": "No results found for $query" + }, + "deezer": { + "command": "deezer", + "description": "Plays music from Deezer", + "searching": "Searching for $query on Deezer...", + "noResults": "No results found for $query" + } + } } } }, diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart new file mode 100644 index 0000000..d4daeef --- /dev/null +++ b/lib/i18n/strings_en.g.dart @@ -0,0 +1,599 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +part of 'strings.g.dart'; + +// Path: +typedef TranslationsEn = Translations; // ignore: unused_element +class Translations implements BaseTranslations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + Translations({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + dynamic operator[](String key) => $meta.getTranslation(key); + + late final Translations _root = this; // ignore: unused_field + + // Translations + late final TranslationsCommandsEn commands = TranslationsCommandsEn.internal(_root); + late final TranslationsServicesEn services = TranslationsServicesEn.internal(_root); + late final TranslationsErrorHandlerEn errorHandler = TranslationsErrorHandlerEn.internal(_root); +} + +// Path: commands +class TranslationsCommandsEn { + TranslationsCommandsEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + late final TranslationsCommandsInfoEn info = TranslationsCommandsInfoEn.internal(_root); + late final TranslationsCommandsSkipEn skip = TranslationsCommandsSkipEn.internal(_root); + late final TranslationsCommandsStopEn stop = TranslationsCommandsStopEn.internal(_root); + late final TranslationsCommandsLeaveEn leave = TranslationsCommandsLeaveEn.internal(_root); + late final TranslationsCommandsJoinEn join = TranslationsCommandsJoinEn.internal(_root); + late final TranslationsCommandsVolumeEn volume = TranslationsCommandsVolumeEn.internal(_root); + late final TranslationsCommandsPauseEn pause = TranslationsCommandsPauseEn.internal(_root); + late final TranslationsCommandsResumeEn resume = TranslationsCommandsResumeEn.internal(_root); + late final TranslationsCommandsMusicEn music = TranslationsCommandsMusicEn.internal(_root); + late final TranslationsCommandsRadioEn radio = TranslationsCommandsRadioEn.internal(_root); +} + +// Path: services +class TranslationsServicesEn { + TranslationsServicesEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + late final TranslationsServicesMusicEn music = TranslationsServicesMusicEn.internal(_root); +} + +// Path: errorHandler +class TranslationsErrorHandlerEn { + TranslationsErrorHandlerEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'An error has occurred'; + String get fallbackDescription => 'Your command couldn\'t be executed because of an error. Please contact a developer for more information.'; + String get musicConnectedToVC => 'I have to be in a voice channel to use this command'; + String get musicNotConnectedToVC => 'I\'m already connected to a voice channel'; + String get musicSameVC => 'I\'m already being used on other voice channel'; + String get musicUserConnectedToVC => 'You need to be connected to a voice channel to use this command'; + late final TranslationsErrorHandlerCooldownEn cooldown = TranslationsErrorHandlerCooldownEn.internal(_root); + late final TranslationsErrorHandlerUnauthorizedCommandEn unauthorizedCommand = TranslationsErrorHandlerUnauthorizedCommandEn.internal(_root); + late final TranslationsErrorHandlerMissingArgumentsEn missingArguments = TranslationsErrorHandlerMissingArgumentsEn.internal(_root); + late final TranslationsErrorHandlerInputParsingFailureEn inputParsingFailure = TranslationsErrorHandlerInputParsingFailureEn.internal(_root); +} + +// Path: commands.info +class TranslationsCommandsInfoEn { + TranslationsCommandsInfoEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'info'; + String get description => 'Show information about the current project'; + String get addToServer => 'Add Radio Horizon to your server'; + String shardOf({required Object index, required Object count}) => 'Shard ${index} of ${count}'; + String get cachedGuilds => 'Cached guilds'; + String get cachedUsers => 'Cached users'; + String get cachedChannels => 'Cached channels'; + String get cachedVoiceStates => 'Cached voice states'; + String get shardCount => 'Shard count'; + String get cachedMessages => 'Cached messages'; + String get memoryUsage => 'Memory usage (current/RSS)'; + String get uptime => 'Uptime'; + String get currentPlayers => 'Current players'; + String get gatewayLatency => 'Gateway latency'; + String get viewOnGithub => 'View on GitHub'; +} + +// Path: commands.skip +class TranslationsCommandsSkipEn { + TranslationsCommandsSkipEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'skip'; + String get description => 'Skips the current song'; + String get skipped => 'Skipped current track'; + String get nothingPlaying => 'The queue is clear!'; +} + +// Path: commands.stop +class TranslationsCommandsStopEn { + TranslationsCommandsStopEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'stop'; + String get description => 'Stops the current player and clears its track queue'; + String get stopped => 'Player stopped'; +} + +// Path: commands.leave +class TranslationsCommandsLeaveEn { + TranslationsCommandsLeaveEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'leave'; + String get description => 'Leaves the current voice channel'; + String get left => 'Left voice channel'; + String get leftDueToInactivity => 'Left voice channel due to inactivity'; +} + +// Path: commands.join +class TranslationsCommandsJoinEn { + TranslationsCommandsJoinEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'join'; + String get description => 'Joins the voice channel you are in'; + String get joined => 'Joined voice channel'; +} + +// Path: commands.volume +class TranslationsCommandsVolumeEn { + TranslationsCommandsVolumeEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'volume'; + String get description => 'Sets the volume of the current player'; + String get volumeDescription => 'The volume to set, this value must be contained between 0 and 1000'; + String volumeSet({required Object volume}) => 'Volume set to ${volume}'; +} + +// Path: commands.pause +class TranslationsCommandsPauseEn { + TranslationsCommandsPauseEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'pause'; + String get description => 'Pauses the current player'; + String get paused => 'Player paused'; +} + +// Path: commands.resume +class TranslationsCommandsResumeEn { + TranslationsCommandsResumeEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'resume'; + String get description => 'Resumes the current player'; + String get resumed => 'Player resumed'; +} + +// Path: commands.music +class TranslationsCommandsMusicEn { + TranslationsCommandsMusicEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'music'; + String get description => 'Music related commands'; + late final TranslationsCommandsMusicChildrenEn children = TranslationsCommandsMusicChildrenEn.internal(_root); +} + +// Path: commands.radio +class TranslationsCommandsRadioEn { + TranslationsCommandsRadioEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'radio'; + String get description => 'Radio related commands'; + late final TranslationsCommandsRadioChildrenEn children = TranslationsCommandsRadioChildrenEn.internal(_root); +} + +// Path: services.music +class TranslationsServicesMusicEn { + TranslationsServicesMusicEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + late final TranslationsServicesMusicTrackStuckEn trackStuck = TranslationsServicesMusicTrackStuckEn.internal(_root); + late final TranslationsServicesMusicTrackStartedEn trackStarted = TranslationsServicesMusicTrackStartedEn.internal(_root); + late final TranslationsServicesMusicTrackExceptionEn trackException = TranslationsServicesMusicTrackExceptionEn.internal(_root); +} + +// Path: errorHandler.cooldown +class TranslationsErrorHandlerCooldownEn { + TranslationsErrorHandlerCooldownEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Command on cooldown'; + String description({required Object inSeconds}) => 'You can\'t use this command right now because it is on cooldown. Please wait ${inSeconds}s and try again.'; +} + +// Path: errorHandler.unauthorizedCommand +class TranslationsErrorHandlerUnauthorizedCommandEn { + TranslationsErrorHandlerUnauthorizedCommandEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'You can\'t use this command!'; + String get description => 'This command can only be used by certain users in certain contexts. Check that you have permission to execute the command, or contact a developer for more information.'; +} + +// Path: errorHandler.missingArguments +class TranslationsErrorHandlerMissingArgumentsEn { + TranslationsErrorHandlerMissingArgumentsEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Not enough arguments'; + String get description => 'You didn\'t provide enough arguments for this command. Please try again and use the Slash Command menu for help, or contact a developer for more information.'; +} + +// Path: errorHandler.inputParsingFailure +class TranslationsErrorHandlerInputParsingFailureEn { + TranslationsErrorHandlerInputParsingFailureEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Couldn\'t parse input'; + String get description => 'Your command couldn\'t be executed because we were unable to understand your input. Please try again with different inputs or contact a developer for more information.'; +} + +// Path: commands.music.children +class TranslationsCommandsMusicChildrenEn { + TranslationsCommandsMusicChildrenEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + late final TranslationsCommandsMusicChildrenPlayEn play = TranslationsCommandsMusicChildrenPlayEn.internal(_root); +} + +// Path: commands.radio.children +class TranslationsCommandsRadioChildrenEn { + TranslationsCommandsRadioChildrenEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + late final TranslationsCommandsRadioChildrenPlayEn play = TranslationsCommandsRadioChildrenPlayEn.internal(_root); + late final TranslationsCommandsRadioChildrenRecognizeEn recognize = TranslationsCommandsRadioChildrenRecognizeEn.internal(_root); + late final TranslationsCommandsRadioChildrenUpvoteEn upvote = TranslationsCommandsRadioChildrenUpvoteEn.internal(_root); + late final TranslationsCommandsRadioChildrenPlayRandomEn playRandom = TranslationsCommandsRadioChildrenPlayRandomEn.internal(_root); +} + +// Path: services.music.trackStuck +class TranslationsServicesMusicTrackStuckEn { + TranslationsServicesMusicTrackStuckEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Track stuck'; + String description({required Object track, required Object uri, required Object requester}) => 'Track [${track}](${uri}}) stuck playing.\n\nRequested by <@${requester}>'; +} + +// Path: services.music.trackStarted +class TranslationsServicesMusicTrackStartedEn { + TranslationsServicesMusicTrackStartedEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Track started'; + String description({required Object track, required Object uri, required Object requester}) => 'Track [${track}](${uri}}) started playing.\n\nRequested by <@${requester}>'; +} + +// Path: services.music.trackException +class TranslationsServicesMusicTrackExceptionEn { + TranslationsServicesMusicTrackExceptionEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'Track exception'; + String description({required Object track, required Object uri, required Object requester}) => 'Track [${track}](${uri}}) threw an exception.\n\nRequested by <@${requester}>'; +} + +// Path: commands.music.children.play +class TranslationsCommandsMusicChildrenPlayEn { + TranslationsCommandsMusicChildrenPlayEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'play'; + String get description => 'Plays music based on the given query'; + String get queryDescription => 'The name/url of the song/playlist to play'; + String noResults({required Object query}) => 'No results found for ${query}'; + String playlistEnqueued({required Object name, required Object query}) => 'Playlist ${name} (${query}) enqueued'; + String songEnqueued({required Object title, required Object query}) => 'Song ${title} (${query}) enqueued'; + late final TranslationsCommandsMusicChildrenPlayChildrenEn children = TranslationsCommandsMusicChildrenPlayChildrenEn.internal(_root); +} + +// Path: commands.radio.children.play +class TranslationsCommandsRadioChildrenPlayEn { + TranslationsCommandsRadioChildrenPlayEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'play'; + String get description => 'Plays a radio station based on the given query'; + String get queryDescription => 'The name of the radio station to play'; + String searching({required Object query}) => 'Searching for radio ${query}...'; + String noResults({required Object query}) => 'No results found for ${query}'; + String get startedPlaying => 'Started playing'; + String startedPlayingDescription({required Object radio, required Object mention}) => 'Radio ${radio} started playing.\n\nRequested by ${mention}'; + String stationEnqueued({required Object name, required Object query}) => 'Station ${name} (${query}) enqueued'; +} + +// Path: commands.radio.children.recognize +class TranslationsCommandsRadioChildrenRecognizeEn { + TranslationsCommandsRadioChildrenRecognizeEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'recognize'; + String get description => 'Recognizes the current song playing'; + String requestedBy({required Object mention}) => 'Requested by ${mention}'; + String get radioStationField => 'Radio Station'; + String get genreField => 'Genre'; + String get computationalTimeField => 'Computational time'; + late final TranslationsCommandsRadioChildrenRecognizeErrorsEn errors = TranslationsCommandsRadioChildrenRecognizeErrorsEn.internal(_root); +} + +// Path: commands.radio.children.upvote +class TranslationsCommandsRadioChildrenUpvoteEn { + TranslationsCommandsRadioChildrenUpvoteEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'upvote'; + String get description => 'Upvotes the current radio playing'; + String requestedBy({required Object mention}) => 'Requested by ${mention}'; + String get success => 'Voted successfully'; + String successDescription({required Object radio}) => 'You have successfully voted for the radio ${radio}! Thank you for your support :D'; + late final TranslationsCommandsRadioChildrenUpvoteErrorsEn errors = TranslationsCommandsRadioChildrenUpvoteErrorsEn.internal(_root); +} + +// Path: commands.radio.children.playRandom +class TranslationsCommandsRadioChildrenPlayRandomEn { + TranslationsCommandsRadioChildrenPlayRandomEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'play-random'; + String get description => 'Plays a random radio station'; + String get searching => 'Searching for a random radio station...'; + String get startedPlaying => 'Started playing'; + String startedPlayingDescription({required Object radio, required Object mention}) => 'Radio ${radio} started playing.\n\nRequested by ${mention}'; + late final TranslationsCommandsRadioChildrenPlayRandomErrorsEn errors = TranslationsCommandsRadioChildrenPlayRandomErrorsEn.internal(_root); +} + +// Path: commands.music.children.play.children +class TranslationsCommandsMusicChildrenPlayChildrenEn { + TranslationsCommandsMusicChildrenPlayChildrenEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + late final TranslationsCommandsMusicChildrenPlayChildrenYoutubeEn youtube = TranslationsCommandsMusicChildrenPlayChildrenYoutubeEn.internal(_root); + late final TranslationsCommandsMusicChildrenPlayChildrenDeezerEn deezer = TranslationsCommandsMusicChildrenPlayChildrenDeezerEn.internal(_root); +} + +// Path: commands.radio.children.recognize.errors +class TranslationsCommandsRadioChildrenRecognizeErrorsEn { + TranslationsCommandsRadioChildrenRecognizeErrorsEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get title => 'An error occurred while recognizing the song'; + String get noRadioPlaying => 'Couldn\'t find a radio playing!'; + String get radioCantCommunicate => 'There was an error communicating with the server, please try again.'; + String get noResults => 'Couldn\'t identify the current song playing :('; +} + +// Path: commands.radio.children.upvote.errors +class TranslationsCommandsRadioChildrenUpvoteErrorsEn { + TranslationsCommandsRadioChildrenUpvoteErrorsEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get noRadioPlaying => 'Couldn\'t find a radio playing!'; +} + +// Path: commands.radio.children.playRandom.errors +class TranslationsCommandsRadioChildrenPlayRandomErrorsEn { + TranslationsCommandsRadioChildrenPlayRandomErrorsEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get noResults => 'Couldn\'t find a random radio station :( Try again later!'; +} + +// Path: commands.music.children.play.children.youtube +class TranslationsCommandsMusicChildrenPlayChildrenYoutubeEn { + TranslationsCommandsMusicChildrenPlayChildrenYoutubeEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'youtube'; + String get description => 'Plays music from YouTube'; + String searching({required Object query}) => 'Searching for ${query} on YouTube...'; + String noResults({required Object query}) => 'No results found for ${query}'; +} + +// Path: commands.music.children.play.children.deezer +class TranslationsCommandsMusicChildrenPlayChildrenDeezerEn { + TranslationsCommandsMusicChildrenPlayChildrenDeezerEn.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get command => 'deezer'; + String get description => 'Plays music from Deezer'; + String searching({required Object query}) => 'Searching for ${query} on Deezer...'; + String noResults({required Object query}) => 'No results found for ${query}'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on Translations { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'commands.info.command': return 'info'; + case 'commands.info.description': return 'Show information about the current project'; + case 'commands.info.addToServer': return 'Add Radio Horizon to your server'; + case 'commands.info.shardOf': return ({required Object index, required Object count}) => 'Shard ${index} of ${count}'; + case 'commands.info.cachedGuilds': return 'Cached guilds'; + case 'commands.info.cachedUsers': return 'Cached users'; + case 'commands.info.cachedChannels': return 'Cached channels'; + case 'commands.info.cachedVoiceStates': return 'Cached voice states'; + case 'commands.info.shardCount': return 'Shard count'; + case 'commands.info.cachedMessages': return 'Cached messages'; + case 'commands.info.memoryUsage': return 'Memory usage (current/RSS)'; + case 'commands.info.uptime': return 'Uptime'; + case 'commands.info.currentPlayers': return 'Current players'; + case 'commands.info.gatewayLatency': return 'Gateway latency'; + case 'commands.info.viewOnGithub': return 'View on GitHub'; + case 'commands.skip.command': return 'skip'; + case 'commands.skip.description': return 'Skips the current song'; + case 'commands.skip.skipped': return 'Skipped current track'; + case 'commands.skip.nothingPlaying': return 'The queue is clear!'; + case 'commands.stop.command': return 'stop'; + case 'commands.stop.description': return 'Stops the current player and clears its track queue'; + case 'commands.stop.stopped': return 'Player stopped'; + case 'commands.leave.command': return 'leave'; + case 'commands.leave.description': return 'Leaves the current voice channel'; + case 'commands.leave.left': return 'Left voice channel'; + case 'commands.leave.leftDueToInactivity': return 'Left voice channel due to inactivity'; + case 'commands.join.command': return 'join'; + case 'commands.join.description': return 'Joins the voice channel you are in'; + case 'commands.join.joined': return 'Joined voice channel'; + case 'commands.volume.command': return 'volume'; + case 'commands.volume.description': return 'Sets the volume of the current player'; + case 'commands.volume.volumeDescription': return 'The volume to set, this value must be contained between 0 and 1000'; + case 'commands.volume.volumeSet': return ({required Object volume}) => 'Volume set to ${volume}'; + case 'commands.pause.command': return 'pause'; + case 'commands.pause.description': return 'Pauses the current player'; + case 'commands.pause.paused': return 'Player paused'; + case 'commands.resume.command': return 'resume'; + case 'commands.resume.description': return 'Resumes the current player'; + case 'commands.resume.resumed': return 'Player resumed'; + case 'commands.music.command': return 'music'; + case 'commands.music.description': return 'Music related commands'; + case 'commands.music.children.play.command': return 'play'; + case 'commands.music.children.play.description': return 'Plays music based on the given query'; + case 'commands.music.children.play.queryDescription': return 'The name/url of the song/playlist to play'; + case 'commands.music.children.play.noResults': return ({required Object query}) => 'No results found for ${query}'; + case 'commands.music.children.play.playlistEnqueued': return ({required Object name, required Object query}) => 'Playlist ${name} (${query}) enqueued'; + case 'commands.music.children.play.songEnqueued': return ({required Object title, required Object query}) => 'Song ${title} (${query}) enqueued'; + case 'commands.music.children.play.children.youtube.command': return 'youtube'; + case 'commands.music.children.play.children.youtube.description': return 'Plays music from YouTube'; + case 'commands.music.children.play.children.youtube.searching': return ({required Object query}) => 'Searching for ${query} on YouTube...'; + case 'commands.music.children.play.children.youtube.noResults': return ({required Object query}) => 'No results found for ${query}'; + case 'commands.music.children.play.children.deezer.command': return 'deezer'; + case 'commands.music.children.play.children.deezer.description': return 'Plays music from Deezer'; + case 'commands.music.children.play.children.deezer.searching': return ({required Object query}) => 'Searching for ${query} on Deezer...'; + case 'commands.music.children.play.children.deezer.noResults': return ({required Object query}) => 'No results found for ${query}'; + case 'commands.radio.command': return 'radio'; + case 'commands.radio.description': return 'Radio related commands'; + case 'commands.radio.children.play.command': return 'play'; + case 'commands.radio.children.play.description': return 'Plays a radio station based on the given query'; + case 'commands.radio.children.play.queryDescription': return 'The name of the radio station to play'; + case 'commands.radio.children.play.searching': return ({required Object query}) => 'Searching for radio ${query}...'; + case 'commands.radio.children.play.noResults': return ({required Object query}) => 'No results found for ${query}'; + case 'commands.radio.children.play.startedPlaying': return 'Started playing'; + case 'commands.radio.children.play.startedPlayingDescription': return ({required Object radio, required Object mention}) => 'Radio ${radio} started playing.\n\nRequested by ${mention}'; + case 'commands.radio.children.play.stationEnqueued': return ({required Object name, required Object query}) => 'Station ${name} (${query}) enqueued'; + case 'commands.radio.children.recognize.command': return 'recognize'; + case 'commands.radio.children.recognize.description': return 'Recognizes the current song playing'; + case 'commands.radio.children.recognize.requestedBy': return ({required Object mention}) => 'Requested by ${mention}'; + case 'commands.radio.children.recognize.radioStationField': return 'Radio Station'; + case 'commands.radio.children.recognize.genreField': return 'Genre'; + case 'commands.radio.children.recognize.computationalTimeField': return 'Computational time'; + case 'commands.radio.children.recognize.errors.title': return 'An error occurred while recognizing the song'; + case 'commands.radio.children.recognize.errors.noRadioPlaying': return 'Couldn\'t find a radio playing!'; + case 'commands.radio.children.recognize.errors.radioCantCommunicate': return 'There was an error communicating with the server, please try again.'; + case 'commands.radio.children.recognize.errors.noResults': return 'Couldn\'t identify the current song playing :('; + case 'commands.radio.children.upvote.command': return 'upvote'; + case 'commands.radio.children.upvote.description': return 'Upvotes the current radio playing'; + case 'commands.radio.children.upvote.requestedBy': return ({required Object mention}) => 'Requested by ${mention}'; + case 'commands.radio.children.upvote.success': return 'Voted successfully'; + case 'commands.radio.children.upvote.successDescription': return ({required Object radio}) => 'You have successfully voted for the radio ${radio}! Thank you for your support :D'; + case 'commands.radio.children.upvote.errors.noRadioPlaying': return 'Couldn\'t find a radio playing!'; + case 'commands.radio.children.playRandom.command': return 'play-random'; + case 'commands.radio.children.playRandom.description': return 'Plays a random radio station'; + case 'commands.radio.children.playRandom.searching': return 'Searching for a random radio station...'; + case 'commands.radio.children.playRandom.startedPlaying': return 'Started playing'; + case 'commands.radio.children.playRandom.startedPlayingDescription': return ({required Object radio, required Object mention}) => 'Radio ${radio} started playing.\n\nRequested by ${mention}'; + case 'commands.radio.children.playRandom.errors.noResults': return 'Couldn\'t find a random radio station :( Try again later!'; + case 'services.music.trackStuck.title': return 'Track stuck'; + case 'services.music.trackStuck.description': return ({required Object track, required Object uri, required Object requester}) => 'Track [${track}](${uri}}) stuck playing.\n\nRequested by <@${requester}>'; + case 'services.music.trackStarted.title': return 'Track started'; + case 'services.music.trackStarted.description': return ({required Object track, required Object uri, required Object requester}) => 'Track [${track}](${uri}}) started playing.\n\nRequested by <@${requester}>'; + case 'services.music.trackException.title': return 'Track exception'; + case 'services.music.trackException.description': return ({required Object track, required Object uri, required Object requester}) => 'Track [${track}](${uri}}) threw an exception.\n\nRequested by <@${requester}>'; + case 'errorHandler.title': return 'An error has occurred'; + case 'errorHandler.fallbackDescription': return 'Your command couldn\'t be executed because of an error. Please contact a developer for more information.'; + case 'errorHandler.musicConnectedToVC': return 'I have to be in a voice channel to use this command'; + case 'errorHandler.musicNotConnectedToVC': return 'I\'m already connected to a voice channel'; + case 'errorHandler.musicSameVC': return 'I\'m already being used on other voice channel'; + case 'errorHandler.musicUserConnectedToVC': return 'You need to be connected to a voice channel to use this command'; + case 'errorHandler.cooldown.title': return 'Command on cooldown'; + case 'errorHandler.cooldown.description': return ({required Object inSeconds}) => 'You can\'t use this command right now because it is on cooldown. Please wait ${inSeconds}s and try again.'; + case 'errorHandler.unauthorizedCommand.title': return 'You can\'t use this command!'; + case 'errorHandler.unauthorizedCommand.description': return 'This command can only be used by certain users in certain contexts. Check that you have permission to execute the command, or contact a developer for more information.'; + case 'errorHandler.missingArguments.title': return 'Not enough arguments'; + case 'errorHandler.missingArguments.description': return 'You didn\'t provide enough arguments for this command. Please try again and use the Slash Command menu for help, or contact a developer for more information.'; + case 'errorHandler.inputParsingFailure.title': return 'Couldn\'t parse input'; + case 'errorHandler.inputParsingFailure.description': return 'Your command couldn\'t be executed because we were unable to understand your input. Please try again with different inputs or contact a developer for more information.'; + default: return null; + } + } +} + diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart new file mode 100644 index 0000000..23fa9e7 --- /dev/null +++ b/lib/i18n/strings_es.g.dart @@ -0,0 +1,602 @@ +/// +/// Generated file. Do not edit. +/// +// coverage:ignore-file +// ignore_for_file: type=lint, unused_import + +import 'package:intl/intl.dart'; +import 'package:slang/generated.dart'; +import 'strings.g.dart'; + +// Path: +class TranslationsEs extends Translations { + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + TranslationsEs({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.es, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ), + super(cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver) { + super.$meta.setFlatMapFunction($meta.getTranslation); // copy base translations to super.$meta + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key) ?? super.$meta.getTranslation(key); + + late final TranslationsEs _root = this; // ignore: unused_field + + // Translations + @override late final TranslationsCommandsEs commands = TranslationsCommandsEs._(_root); + @override late final TranslationsServicesEs services = TranslationsServicesEs._(_root); + @override late final TranslationsErrorHandlerEs errorHandler = TranslationsErrorHandlerEs._(_root); +} + +// Path: commands +class TranslationsCommandsEs extends TranslationsCommandsEn { + TranslationsCommandsEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override late final TranslationsCommandsInfoEs info = TranslationsCommandsInfoEs._(_root); + @override late final TranslationsCommandsSkipEs skip = TranslationsCommandsSkipEs._(_root); + @override late final TranslationsCommandsStopEs stop = TranslationsCommandsStopEs._(_root); + @override late final TranslationsCommandsLeaveEs leave = TranslationsCommandsLeaveEs._(_root); + @override late final TranslationsCommandsJoinEs join = TranslationsCommandsJoinEs._(_root); + @override late final TranslationsCommandsVolumeEs volume = TranslationsCommandsVolumeEs._(_root); + @override late final TranslationsCommandsPauseEs pause = TranslationsCommandsPauseEs._(_root); + @override late final TranslationsCommandsResumeEs resume = TranslationsCommandsResumeEs._(_root); + @override late final TranslationsCommandsMusicEs music = TranslationsCommandsMusicEs._(_root); + @override late final TranslationsCommandsRadioEs radio = TranslationsCommandsRadioEs._(_root); +} + +// Path: services +class TranslationsServicesEs extends TranslationsServicesEn { + TranslationsServicesEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override late final TranslationsServicesMusicEs music = TranslationsServicesMusicEs._(_root); +} + +// Path: errorHandler +class TranslationsErrorHandlerEs extends TranslationsErrorHandlerEn { + TranslationsErrorHandlerEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get title => 'Ha ocurrido un error'; + @override String get fallbackDescription => 'Tu comando no se ha podido ejecutar debido a un error desconocido. Por favor contacte a un desarrollador para más información.'; + @override String get musicConnectedToVC => 'Debo estar conectado a un canal de voz para ejecutar este comando'; + @override String get musicNotConnectedToVC => 'Ya estoy conectado a un canal de voz'; + @override String get musicSameVC => 'Debes estar en el mismo canal de voz que yo para ejecutar este comando'; + @override String get musicUserConnectedToVC => 'Debes estar conectado a un canal de voz para ejecutar este comando'; + @override late final TranslationsErrorHandlerCooldownEs cooldown = TranslationsErrorHandlerCooldownEs._(_root); + @override late final TranslationsErrorHandlerUnauthorizedCommandEs unauthorizedCommand = TranslationsErrorHandlerUnauthorizedCommandEs._(_root); + @override late final TranslationsErrorHandlerMissingArgumentsEs missingArguments = TranslationsErrorHandlerMissingArgumentsEs._(_root); + @override late final TranslationsErrorHandlerInputParsingFailureEs inputParsingFailure = TranslationsErrorHandlerInputParsingFailureEs._(_root); +} + +// Path: commands.info +class TranslationsCommandsInfoEs extends TranslationsCommandsInfoEn { + TranslationsCommandsInfoEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'info'; + @override String get description => 'Muestra información sobre el bot'; + @override String get addToServer => 'Añadir Radio Horizon a mi servidor'; + @override String shardOf({required Object index, required Object count}) => 'Fragmento ${index} de ${count}'; + @override String get cachedGuilds => 'Servidores en caché'; + @override String get cachedUsers => 'Usuarios en caché'; + @override String get cachedChannels => 'Canales en caché'; + @override String get cachedVoiceStates => 'Estados de voz en caché'; + @override String get shardCount => 'Fragmentos'; + @override String get cachedMessages => 'Mensajes en caché'; + @override String get memoryUsage => 'Uso de memoria (current/RSS)'; + @override String get uptime => 'Tiempo de actividad'; + @override String get currentPlayers => 'Reproductores actuales'; + @override String get gatewayLatency => 'Latencia de la puerta de enlace'; + @override String get viewOnGithub => 'Ver en GitHub'; +} + +// Path: commands.skip +class TranslationsCommandsSkipEs extends TranslationsCommandsSkipEn { + TranslationsCommandsSkipEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'saltar'; + @override String get description => 'Salta a la siguiente canción en la cola'; + @override String get skipped => 'La canción actual ha sido saltada'; + @override String get nothingPlaying => 'La cola está vacía'; +} + +// Path: commands.stop +class TranslationsCommandsStopEs extends TranslationsCommandsStopEn { + TranslationsCommandsStopEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'parar'; + @override String get description => 'Para la reproducción de música'; + @override String get stopped => 'La reproducción de música ha sido detenida'; +} + +// Path: commands.leave +class TranslationsCommandsLeaveEs extends TranslationsCommandsLeaveEn { + TranslationsCommandsLeaveEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'dejar'; + @override String get description => 'Abandona el canal de voz'; + @override String get left => 'El canal de voz ha sido abandonado'; + @override String get leftDueToInactivity => 'El canal de voz ha sido abandonado debido a la inactividad'; +} + +// Path: commands.join +class TranslationsCommandsJoinEs extends TranslationsCommandsJoinEn { + TranslationsCommandsJoinEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'unirse'; + @override String get description => 'Se une al canal de voz en el que estás'; + @override String get joined => 'Se ha unido al canal de voz'; +} + +// Path: commands.volume +class TranslationsCommandsVolumeEs extends TranslationsCommandsVolumeEn { + TranslationsCommandsVolumeEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'volumen'; + @override String get description => 'Establece el volumen de la música'; + @override String get volumeDescription => 'El volumen para establecer, debe ser un valor entre 0 y 1000'; + @override String volumeSet({required Object volume}) => 'Volumen puesto en ${volume}'; +} + +// Path: commands.pause +class TranslationsCommandsPauseEs extends TranslationsCommandsPauseEn { + TranslationsCommandsPauseEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'pausar'; + @override String get description => 'Pausa la reproducción de música'; + @override String get paused => 'La reproducción de música ha sido pausada'; +} + +// Path: commands.resume +class TranslationsCommandsResumeEs extends TranslationsCommandsResumeEn { + TranslationsCommandsResumeEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'reanudar'; + @override String get description => 'Reanuda la reproducción de música'; + @override String get resumed => 'La reproducción de música ha sido reanudada'; +} + +// Path: commands.music +class TranslationsCommandsMusicEs extends TranslationsCommandsMusicEn { + TranslationsCommandsMusicEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'musica'; + @override String get description => 'Comandos relacionados con la funcionalidad de música'; + @override late final TranslationsCommandsMusicChildrenEs children = TranslationsCommandsMusicChildrenEs._(_root); +} + +// Path: commands.radio +class TranslationsCommandsRadioEs extends TranslationsCommandsRadioEn { + TranslationsCommandsRadioEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'radio'; + @override String get description => 'Comandos relacionados con la funcionalidad de radio'; + @override late final TranslationsCommandsRadioChildrenEs children = TranslationsCommandsRadioChildrenEs._(_root); +} + +// Path: services.music +class TranslationsServicesMusicEs extends TranslationsServicesMusicEn { + TranslationsServicesMusicEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override late final TranslationsServicesMusicTrackStuckEs trackStuck = TranslationsServicesMusicTrackStuckEs._(_root); + @override late final TranslationsServicesMusicTrackStartedEs trackStarted = TranslationsServicesMusicTrackStartedEs._(_root); + @override late final TranslationsServicesMusicTrackExceptionEs trackException = TranslationsServicesMusicTrackExceptionEs._(_root); +} + +// Path: errorHandler.cooldown +class TranslationsErrorHandlerCooldownEs extends TranslationsErrorHandlerCooldownEn { + TranslationsErrorHandlerCooldownEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get title => 'Commando en cooldown'; + @override String description({required Object inSeconds}) => 'No puedes usar este comando ahora mismo porque está en cooldown. Por favor espera ${inSeconds}s e inténtalo de nuevo.'; +} + +// Path: errorHandler.unauthorizedCommand +class TranslationsErrorHandlerUnauthorizedCommandEs extends TranslationsErrorHandlerUnauthorizedCommandEn { + TranslationsErrorHandlerUnauthorizedCommandEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get title => 'No puedes usar este comando!'; + @override String get description => 'Este comando solo puede ser usado por determinados usuarios en determinados contextos. Verifica que tienes los permisos para ejecutar este comando, o contacte a un desarrollador para más información.'; +} + +// Path: errorHandler.missingArguments +class TranslationsErrorHandlerMissingArgumentsEs extends TranslationsErrorHandlerMissingArgumentsEn { + TranslationsErrorHandlerMissingArgumentsEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get title => 'Faltan argumentos'; + @override String get description => 'No has proveido los argumentos necesarios para ejecutar esta función. Por favor inténtalo de nuevo y usa el Menú de Slash Command para ayuda, o contacta a un desarrollador para más información.'; +} + +// Path: errorHandler.inputParsingFailure +class TranslationsErrorHandlerInputParsingFailureEs extends TranslationsErrorHandlerInputParsingFailureEn { + TranslationsErrorHandlerInputParsingFailureEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get title => 'Ha ocurrido un error al procesar tu entrada'; + @override String get description => 'No se ha podido ejecutar el comando ya que no hemos podido interpretar tus argumentos. Por favor inténtalo de nuevo y usa el Menú de Slash Command para ayuda, o contacta a un desarrollador para más información.'; +} + +// Path: commands.music.children +class TranslationsCommandsMusicChildrenEs extends TranslationsCommandsMusicChildrenEn { + TranslationsCommandsMusicChildrenEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override late final TranslationsCommandsMusicChildrenPlayEs play = TranslationsCommandsMusicChildrenPlayEs._(_root); +} + +// Path: commands.radio.children +class TranslationsCommandsRadioChildrenEs extends TranslationsCommandsRadioChildrenEn { + TranslationsCommandsRadioChildrenEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override late final TranslationsCommandsRadioChildrenPlayEs play = TranslationsCommandsRadioChildrenPlayEs._(_root); + @override late final TranslationsCommandsRadioChildrenRecognizeEs recognize = TranslationsCommandsRadioChildrenRecognizeEs._(_root); + @override late final TranslationsCommandsRadioChildrenUpvoteEs upvote = TranslationsCommandsRadioChildrenUpvoteEs._(_root); + @override late final TranslationsCommandsRadioChildrenPlayRandomEs playRandom = TranslationsCommandsRadioChildrenPlayRandomEs._(_root); +} + +// Path: services.music.trackStuck +class TranslationsServicesMusicTrackStuckEs extends TranslationsServicesMusicTrackStuckEn { + TranslationsServicesMusicTrackStuckEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get title => 'La canción se ha quedado atascada'; + @override String description({required Object track, required Object uri, required Object requester}) => 'La pista [${track}](${uri}}) se ha quedado atascada.\n\nPedida por <@${requester}>'; +} + +// Path: services.music.trackStarted +class TranslationsServicesMusicTrackStartedEs extends TranslationsServicesMusicTrackStartedEn { + TranslationsServicesMusicTrackStartedEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get title => 'Se ha comenzado a reproducir'; + @override String description({required Object track, required Object uri, required Object requester}) => 'La pista [${track}](${uri}}) se ha comenzado a reproducir.\n\nPedido por <@${requester}>'; +} + +// Path: services.music.trackException +class TranslationsServicesMusicTrackExceptionEs extends TranslationsServicesMusicTrackExceptionEn { + TranslationsServicesMusicTrackExceptionEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get title => 'Ha ocurrido un error al reproducir la canción'; + @override String description({required Object track, required Object uri, required Object requester}) => 'La pista [${track}](${uri}}) devolvió un error.\n\nPedido por <@${requester}>'; +} + +// Path: commands.music.children.play +class TranslationsCommandsMusicChildrenPlayEs extends TranslationsCommandsMusicChildrenPlayEn { + TranslationsCommandsMusicChildrenPlayEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'reproducir'; + @override String get description => 'Reproduce música basada en una URL o una búsqueda'; + @override String get queryDescription => 'El nombre de una canción o una URL'; + @override String noResults({required Object query}) => 'No se encontraron resultados para ${query}'; + @override String playlistEnqueued({required Object name, required Object query}) => 'Lista de reproducción ${name} (${query}) añadida a la cola'; + @override String songEnqueued({required Object title, required Object query}) => 'Canción ${title} (${query}) añadida a la cola'; + @override late final TranslationsCommandsMusicChildrenPlayChildrenEs children = TranslationsCommandsMusicChildrenPlayChildrenEs._(_root); +} + +// Path: commands.radio.children.play +class TranslationsCommandsRadioChildrenPlayEs extends TranslationsCommandsRadioChildrenPlayEn { + TranslationsCommandsRadioChildrenPlayEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'reproducir'; + @override String get description => 'Reproduce una radio basada en una búsqueda'; + @override String get queryDescription => 'El nombre de una estación de radio'; + @override String searching({required Object query}) => 'Buscando radio ${query}...'; + @override String noResults({required Object query}) => 'No se encontraron resultados para ${query}'; + @override String get startedPlaying => 'Se ha comenzado a reproducir'; + @override String startedPlayingDescription({required Object radio, required Object mention}) => 'La radio ${radio} ha comenzado a reproducirse.\n\nPedido por ${mention}'; + @override String stationEnqueued({required Object name, required Object query}) => 'La radio ${name} (${query}) ha sido añadida a la cola'; +} + +// Path: commands.radio.children.recognize +class TranslationsCommandsRadioChildrenRecognizeEs extends TranslationsCommandsRadioChildrenRecognizeEn { + TranslationsCommandsRadioChildrenRecognizeEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'reconocer'; + @override String get description => 'Reconoce la cancion que se está reproduciendo en la radio'; + @override String requestedBy({required Object mention}) => 'Pedido por ${mention}'; + @override String get radioStationField => 'Estación'; + @override String get genreField => 'Género'; + @override String get computationalTimeField => 'Tiempo de cálculo'; + @override late final TranslationsCommandsRadioChildrenRecognizeErrorsEs errors = TranslationsCommandsRadioChildrenRecognizeErrorsEs._(_root); +} + +// Path: commands.radio.children.upvote +class TranslationsCommandsRadioChildrenUpvoteEs extends TranslationsCommandsRadioChildrenUpvoteEn { + TranslationsCommandsRadioChildrenUpvoteEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'votar'; + @override String get description => 'Vota positivamente por la radio que se está reproduciendo'; + @override String requestedBy({required Object mention}) => 'Pedido por ${mention}'; + @override String get success => 'Voto positivo añadido'; + @override String successDescription({required Object radio}) => 'Has votado positivamente por la radio ${radio}! Gracias por tu apoyo :D'; + @override late final TranslationsCommandsRadioChildrenUpvoteErrorsEs errors = TranslationsCommandsRadioChildrenUpvoteErrorsEs._(_root); +} + +// Path: commands.radio.children.playRandom +class TranslationsCommandsRadioChildrenPlayRandomEs extends TranslationsCommandsRadioChildrenPlayRandomEn { + TranslationsCommandsRadioChildrenPlayRandomEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'play-random'; + @override String get description => 'Reproduce una radio aleatoria'; + @override String get searching => 'Buscando una radio aleatoria...'; + @override String get startedPlaying => 'Se ha comenzado a reproducir'; + @override String startedPlayingDescription({required Object radio, required Object mention}) => 'La radio ${radio} ha comenzado a reproducirse.\n\nPedido por ${mention}'; + @override late final TranslationsCommandsRadioChildrenPlayRandomErrorsEs errors = TranslationsCommandsRadioChildrenPlayRandomErrorsEs._(_root); +} + +// Path: commands.music.children.play.children +class TranslationsCommandsMusicChildrenPlayChildrenEs extends TranslationsCommandsMusicChildrenPlayChildrenEn { + TranslationsCommandsMusicChildrenPlayChildrenEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override late final TranslationsCommandsMusicChildrenPlayChildrenYoutubeEs youtube = TranslationsCommandsMusicChildrenPlayChildrenYoutubeEs._(_root); + @override late final TranslationsCommandsMusicChildrenPlayChildrenDeezerEs deezer = TranslationsCommandsMusicChildrenPlayChildrenDeezerEs._(_root); +} + +// Path: commands.radio.children.recognize.errors +class TranslationsCommandsRadioChildrenRecognizeErrorsEs extends TranslationsCommandsRadioChildrenRecognizeErrorsEn { + TranslationsCommandsRadioChildrenRecognizeErrorsEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get title => 'Ha ocurrido un error al reconocer la canción'; + @override String get noRadioPlaying => 'No se está reproduciendo ninguna radio'; + @override String get radioCantCommunicate => 'La radio no puede comunicarse con el servidor de reconocimiento de canciones. Inténtalo de nuevo más tarde'; + @override String get noResults => 'No se han encontrado resultados para la canción que se está reproduciendo :('; +} + +// Path: commands.radio.children.upvote.errors +class TranslationsCommandsRadioChildrenUpvoteErrorsEs extends TranslationsCommandsRadioChildrenUpvoteErrorsEn { + TranslationsCommandsRadioChildrenUpvoteErrorsEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get noRadioPlaying => 'No se está reproduciendo ninguna radio'; +} + +// Path: commands.radio.children.playRandom.errors +class TranslationsCommandsRadioChildrenPlayRandomErrorsEs extends TranslationsCommandsRadioChildrenPlayRandomErrorsEn { + TranslationsCommandsRadioChildrenPlayRandomErrorsEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get noResults => 'No se ha podido encontrar una radio aleatoria :( Inténtalo de nuevo más tarde!'; +} + +// Path: commands.music.children.play.children.youtube +class TranslationsCommandsMusicChildrenPlayChildrenYoutubeEs extends TranslationsCommandsMusicChildrenPlayChildrenYoutubeEn { + TranslationsCommandsMusicChildrenPlayChildrenYoutubeEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'youtube'; + @override String get description => 'Reproduce música de YouTube'; + @override String searching({required Object query}) => 'Buscando ${query} en YouTube...'; + @override String noResults({required Object query}) => 'No se encontraron resultados para ${query}'; +} + +// Path: commands.music.children.play.children.deezer +class TranslationsCommandsMusicChildrenPlayChildrenDeezerEs extends TranslationsCommandsMusicChildrenPlayChildrenDeezerEn { + TranslationsCommandsMusicChildrenPlayChildrenDeezerEs._(TranslationsEs root) : this._root = root, super.internal(root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get command => 'deezer'; + @override String get description => 'Reproduce música de Deezer'; + @override String searching({required Object query}) => 'Buscando ${query} en Deezer...'; + @override String noResults({required Object query}) => 'No se encontraron resultados para ${query}'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. +extension on TranslationsEs { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'commands.info.command': return 'info'; + case 'commands.info.description': return 'Muestra información sobre el bot'; + case 'commands.info.addToServer': return 'Añadir Radio Horizon a mi servidor'; + case 'commands.info.shardOf': return ({required Object index, required Object count}) => 'Fragmento ${index} de ${count}'; + case 'commands.info.cachedGuilds': return 'Servidores en caché'; + case 'commands.info.cachedUsers': return 'Usuarios en caché'; + case 'commands.info.cachedChannels': return 'Canales en caché'; + case 'commands.info.cachedVoiceStates': return 'Estados de voz en caché'; + case 'commands.info.shardCount': return 'Fragmentos'; + case 'commands.info.cachedMessages': return 'Mensajes en caché'; + case 'commands.info.memoryUsage': return 'Uso de memoria (current/RSS)'; + case 'commands.info.uptime': return 'Tiempo de actividad'; + case 'commands.info.currentPlayers': return 'Reproductores actuales'; + case 'commands.info.gatewayLatency': return 'Latencia de la puerta de enlace'; + case 'commands.info.viewOnGithub': return 'Ver en GitHub'; + case 'commands.skip.command': return 'saltar'; + case 'commands.skip.description': return 'Salta a la siguiente canción en la cola'; + case 'commands.skip.skipped': return 'La canción actual ha sido saltada'; + case 'commands.skip.nothingPlaying': return 'La cola está vacía'; + case 'commands.stop.command': return 'parar'; + case 'commands.stop.description': return 'Para la reproducción de música'; + case 'commands.stop.stopped': return 'La reproducción de música ha sido detenida'; + case 'commands.leave.command': return 'dejar'; + case 'commands.leave.description': return 'Abandona el canal de voz'; + case 'commands.leave.left': return 'El canal de voz ha sido abandonado'; + case 'commands.leave.leftDueToInactivity': return 'El canal de voz ha sido abandonado debido a la inactividad'; + case 'commands.join.command': return 'unirse'; + case 'commands.join.description': return 'Se une al canal de voz en el que estás'; + case 'commands.join.joined': return 'Se ha unido al canal de voz'; + case 'commands.volume.command': return 'volumen'; + case 'commands.volume.description': return 'Establece el volumen de la música'; + case 'commands.volume.volumeDescription': return 'El volumen para establecer, debe ser un valor entre 0 y 1000'; + case 'commands.volume.volumeSet': return ({required Object volume}) => 'Volumen puesto en ${volume}'; + case 'commands.pause.command': return 'pausar'; + case 'commands.pause.description': return 'Pausa la reproducción de música'; + case 'commands.pause.paused': return 'La reproducción de música ha sido pausada'; + case 'commands.resume.command': return 'reanudar'; + case 'commands.resume.description': return 'Reanuda la reproducción de música'; + case 'commands.resume.resumed': return 'La reproducción de música ha sido reanudada'; + case 'commands.music.command': return 'musica'; + case 'commands.music.description': return 'Comandos relacionados con la funcionalidad de música'; + case 'commands.music.children.play.command': return 'reproducir'; + case 'commands.music.children.play.description': return 'Reproduce música basada en una URL o una búsqueda'; + case 'commands.music.children.play.queryDescription': return 'El nombre de una canción o una URL'; + case 'commands.music.children.play.noResults': return ({required Object query}) => 'No se encontraron resultados para ${query}'; + case 'commands.music.children.play.playlistEnqueued': return ({required Object name, required Object query}) => 'Lista de reproducción ${name} (${query}) añadida a la cola'; + case 'commands.music.children.play.songEnqueued': return ({required Object title, required Object query}) => 'Canción ${title} (${query}) añadida a la cola'; + case 'commands.music.children.play.children.youtube.command': return 'youtube'; + case 'commands.music.children.play.children.youtube.description': return 'Reproduce música de YouTube'; + case 'commands.music.children.play.children.youtube.searching': return ({required Object query}) => 'Buscando ${query} en YouTube...'; + case 'commands.music.children.play.children.youtube.noResults': return ({required Object query}) => 'No se encontraron resultados para ${query}'; + case 'commands.music.children.play.children.deezer.command': return 'deezer'; + case 'commands.music.children.play.children.deezer.description': return 'Reproduce música de Deezer'; + case 'commands.music.children.play.children.deezer.searching': return ({required Object query}) => 'Buscando ${query} en Deezer...'; + case 'commands.music.children.play.children.deezer.noResults': return ({required Object query}) => 'No se encontraron resultados para ${query}'; + case 'commands.radio.command': return 'radio'; + case 'commands.radio.description': return 'Comandos relacionados con la funcionalidad de radio'; + case 'commands.radio.children.play.command': return 'reproducir'; + case 'commands.radio.children.play.description': return 'Reproduce una radio basada en una búsqueda'; + case 'commands.radio.children.play.queryDescription': return 'El nombre de una estación de radio'; + case 'commands.radio.children.play.searching': return ({required Object query}) => 'Buscando radio ${query}...'; + case 'commands.radio.children.play.noResults': return ({required Object query}) => 'No se encontraron resultados para ${query}'; + case 'commands.radio.children.play.startedPlaying': return 'Se ha comenzado a reproducir'; + case 'commands.radio.children.play.startedPlayingDescription': return ({required Object radio, required Object mention}) => 'La radio ${radio} ha comenzado a reproducirse.\n\nPedido por ${mention}'; + case 'commands.radio.children.play.stationEnqueued': return ({required Object name, required Object query}) => 'La radio ${name} (${query}) ha sido añadida a la cola'; + case 'commands.radio.children.recognize.command': return 'reconocer'; + case 'commands.radio.children.recognize.description': return 'Reconoce la cancion que se está reproduciendo en la radio'; + case 'commands.radio.children.recognize.requestedBy': return ({required Object mention}) => 'Pedido por ${mention}'; + case 'commands.radio.children.recognize.radioStationField': return 'Estación'; + case 'commands.radio.children.recognize.genreField': return 'Género'; + case 'commands.radio.children.recognize.computationalTimeField': return 'Tiempo de cálculo'; + case 'commands.radio.children.recognize.errors.title': return 'Ha ocurrido un error al reconocer la canción'; + case 'commands.radio.children.recognize.errors.noRadioPlaying': return 'No se está reproduciendo ninguna radio'; + case 'commands.radio.children.recognize.errors.radioCantCommunicate': return 'La radio no puede comunicarse con el servidor de reconocimiento de canciones. Inténtalo de nuevo más tarde'; + case 'commands.radio.children.recognize.errors.noResults': return 'No se han encontrado resultados para la canción que se está reproduciendo :('; + case 'commands.radio.children.upvote.command': return 'votar'; + case 'commands.radio.children.upvote.description': return 'Vota positivamente por la radio que se está reproduciendo'; + case 'commands.radio.children.upvote.requestedBy': return ({required Object mention}) => 'Pedido por ${mention}'; + case 'commands.radio.children.upvote.success': return 'Voto positivo añadido'; + case 'commands.radio.children.upvote.successDescription': return ({required Object radio}) => 'Has votado positivamente por la radio ${radio}! Gracias por tu apoyo :D'; + case 'commands.radio.children.upvote.errors.noRadioPlaying': return 'No se está reproduciendo ninguna radio'; + case 'commands.radio.children.playRandom.command': return 'play-random'; + case 'commands.radio.children.playRandom.description': return 'Reproduce una radio aleatoria'; + case 'commands.radio.children.playRandom.searching': return 'Buscando una radio aleatoria...'; + case 'commands.radio.children.playRandom.startedPlaying': return 'Se ha comenzado a reproducir'; + case 'commands.radio.children.playRandom.startedPlayingDescription': return ({required Object radio, required Object mention}) => 'La radio ${radio} ha comenzado a reproducirse.\n\nPedido por ${mention}'; + case 'commands.radio.children.playRandom.errors.noResults': return 'No se ha podido encontrar una radio aleatoria :( Inténtalo de nuevo más tarde!'; + case 'services.music.trackStuck.title': return 'La canción se ha quedado atascada'; + case 'services.music.trackStuck.description': return ({required Object track, required Object uri, required Object requester}) => 'La pista [${track}](${uri}}) se ha quedado atascada.\n\nPedida por <@${requester}>'; + case 'services.music.trackStarted.title': return 'Se ha comenzado a reproducir'; + case 'services.music.trackStarted.description': return ({required Object track, required Object uri, required Object requester}) => 'La pista [${track}](${uri}}) se ha comenzado a reproducir.\n\nPedido por <@${requester}>'; + case 'services.music.trackException.title': return 'Ha ocurrido un error al reproducir la canción'; + case 'services.music.trackException.description': return ({required Object track, required Object uri, required Object requester}) => 'La pista [${track}](${uri}}) devolvió un error.\n\nPedido por <@${requester}>'; + case 'errorHandler.title': return 'Ha ocurrido un error'; + case 'errorHandler.fallbackDescription': return 'Tu comando no se ha podido ejecutar debido a un error desconocido. Por favor contacte a un desarrollador para más información.'; + case 'errorHandler.musicConnectedToVC': return 'Debo estar conectado a un canal de voz para ejecutar este comando'; + case 'errorHandler.musicNotConnectedToVC': return 'Ya estoy conectado a un canal de voz'; + case 'errorHandler.musicSameVC': return 'Debes estar en el mismo canal de voz que yo para ejecutar este comando'; + case 'errorHandler.musicUserConnectedToVC': return 'Debes estar conectado a un canal de voz para ejecutar este comando'; + case 'errorHandler.cooldown.title': return 'Commando en cooldown'; + case 'errorHandler.cooldown.description': return ({required Object inSeconds}) => 'No puedes usar este comando ahora mismo porque está en cooldown. Por favor espera ${inSeconds}s e inténtalo de nuevo.'; + case 'errorHandler.unauthorizedCommand.title': return 'No puedes usar este comando!'; + case 'errorHandler.unauthorizedCommand.description': return 'Este comando solo puede ser usado por determinados usuarios en determinados contextos. Verifica que tienes los permisos para ejecutar este comando, o contacte a un desarrollador para más información.'; + case 'errorHandler.missingArguments.title': return 'Faltan argumentos'; + case 'errorHandler.missingArguments.description': return 'No has proveido los argumentos necesarios para ejecutar esta función. Por favor inténtalo de nuevo y usa el Menú de Slash Command para ayuda, o contacta a un desarrollador para más información.'; + case 'errorHandler.inputParsingFailure.title': return 'Ha ocurrido un error al procesar tu entrada'; + case 'errorHandler.inputParsingFailure.description': return 'No se ha podido ejecutar el comando ya que no hemos podido interpretar tus argumentos. Por favor inténtalo de nuevo y usa el Menú de Slash Command para ayuda, o contacta a un desarrollador para más información.'; + default: return null; + } + } +} + diff --git a/lib/i18n/strings_es.i18n.json b/lib/i18n/strings_es.i18n.json index f015a72..fa8f708 100644 --- a/lib/i18n/strings_es.i18n.json +++ b/lib/i18n/strings_es.i18n.json @@ -14,7 +14,8 @@ "memoryUsage": "Uso de memoria (current/RSS)", "uptime": "Tiempo de actividad", "currentPlayers": "Reproductores actuales", - "gatewayLatency": "Latencia de la puerta de enlace" + "gatewayLatency": "Latencia de la puerta de enlace", + "viewOnGithub": "Ver en GitHub" }, "skip": { "command": "saltar", @@ -64,7 +65,21 @@ "queryDescription": "El nombre de una canción o una URL", "noResults": "No se encontraron resultados para $query", "playlistEnqueued": "Lista de reproducción $name (${query}) añadida a la cola", - "songEnqueued": "Canción $title (${query}) añadida a la cola" + "songEnqueued": "Canción $title (${query}) añadida a la cola", + "children": { + "youtube": { + "command": "youtube", + "description": "Reproduce música de YouTube", + "searching": "Buscando $query en YouTube...", + "noResults": "No se encontraron resultados para $query" + }, + "deezer": { + "command": "deezer", + "description": "Reproduce música de Deezer", + "searching": "Buscando $query en Deezer...", + "noResults": "No se encontraron resultados para $query" + } + } } } }, diff --git a/lib/src/checks.dart b/lib/src/checks.dart index 28b1f5f..c5beec1 100644 --- a/lib/src/checks.dart +++ b/lib/src/checks.dart @@ -12,74 +12,51 @@ final administratorCheck = UserCheck.anyId( name: 'Administrator check', ); -final connectedToAVoiceChannelCheck = Check( - (ICommandContextData context) async { - final selfMember = await context.guild?.selfMember.getOrDownload(); - - if (selfMember == null) { +final botConnectedToAVoiceChannelCheck = Check( + (CommandContext context) async { + final guildId = context.guild?.id; + if (guildId == null) { return false; } - if (selfMember.voiceState == null || - selfMember.voiceState?.channel == null) { - return false; - } - return true; + final botVoiceState = context.guild?.voiceStates[context.client.user.id]; + return botVoiceState?.channel != null; }, name: 'musicConnectedToVC', ); -final notConnectedToAVoiceChannelCheck = Check( - (ICommandContextData context) async { - final selfMember = await context.guild?.selfMember.getOrDownload(); - - if (selfMember == null) { +final botNotConnectedToAVoiceChannelCheck = Check( + (CommandContext context) async { + final guildId = context.guild?.id; + if (guildId == null) { return false; } - if (selfMember.voiceState == null || - selfMember.voiceState?.channel == null) { - return true; - } - return false; + final botVoiceState = context.guild?.voiceStates[context.client.user.id]; + + return botVoiceState?.channel == null; }, name: 'musicNotConnectedToVC', ); final userConnectedToVoiceChannelCheck = Check( - (ICommandContextData context) { - final memberVoiceState = context.member?.voiceState; - - if (memberVoiceState == null || memberVoiceState.channel == null) { - return false; - } - return true; + (CommandContext context) async { + final userVoiceState = context.guild?.voiceStates[context.member?.id]; + return userVoiceState != null || userVoiceState?.channel != null; }, name: 'musicUserConnectedToVC', ); final sameVoiceChannelOrDisconnectedCheck = Check( - (ICommandContextData context) async { - // If this is an interaction, acknowledge it just in case the check - // takes too long to run. - if (context is InteractionChatContext) { - await context.acknowledge(); - } + (CommandContext context) async { + final userVoiceState = context.guild?.voiceStates[context.member?.id]; + final botVoiceState = context.guild?.voiceStates[context.client.user.id]; - final selfMemberVoiceState = - (await context.guild?.selfMember.getOrDownload())?.voiceState; - // The upper check should be executed before, so its okay to - // assume the voice state exists. - final memberVoiceState = context.member?.voiceState; - - if (selfMemberVoiceState == null || selfMemberVoiceState.channel == null) { + if (botVoiceState?.channel == null) { return true; } - if (selfMemberVoiceState.channel?.id != memberVoiceState?.channel?.id) { - return false; - } - return true; + return userVoiceState?.channel == botVoiceState?.channel; }, name: 'musicSameVC', ); diff --git a/lib/src/commands/commands.dart b/lib/src/commands/commands.dart index 0446b68..479be18 100644 --- a/lib/src/commands/commands.dart +++ b/lib/src/commands/commands.dart @@ -4,105 +4,68 @@ // 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:collection/collection.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; -import 'package:nyxx_interactions/nyxx_interactions.dart'; +import 'package:nyxx_lavalink/nyxx_lavalink.dart'; import 'package:radio_horizon/radio_horizon.dart'; +import 'package:radio_horizon/src/models/exceptions.dart'; export 'info.dart'; export 'music.dart'; export 'radio.dart'; export 'sound.dart'; -final _getIt = GetIt.instance; - -Future connectIfNeeded( - IChatContext context, { - bool replace = false, -}) async { - if (replace) { - _getIt - .get() - .cluster - .getOrCreatePlayerNode(context.guild!.id) - .destroy(context.guild!.id); - context.guild!.shard.changeVoiceState( - context.guild!.id, - null, - selfDeafen: true, - ); +Future connectLavalink(CommandContext context) async { + final voiceState = context.guild?.voiceStates[context.member?.id]; + if (voiceState == null || voiceState.channel == null) { + throw const UserNotConnectedToVoiceChannelException(); } - await Future.delayed(const Duration(milliseconds: 500)); - - final selfMember = await context.guild!.selfMember.getOrDownload(); - - if ((selfMember.voiceState == null || - selfMember.voiceState?.channel == null) && - (context.member?.voiceState != null && - context.member?.voiceState?.channel != null)) { - context.guild!.shard.changeVoiceState( - context.guild!.id, - context.member!.voiceState!.channel!.id, - selfDeafen: true, - ); - } + final voiceChannel = (await voiceState.channel!.fetch()) as VoiceChannel; + final player = await voiceChannel.connectLavalink(); + return player; } -Future connectToChannel( - IGuild guild, +Future connectToChannel( + Guild guild, Snowflake channelId, { bool replace = false, }) async { - if (replace) { - _getIt - .get() - .cluster - .getOrCreatePlayerNode(guild.id) - .destroy(guild.id); - guild.shard.changeVoiceState( - guild.id, - null, - selfDeafen: true, - ); + final voiceChannels = await guild.fetchChannels(); + final voiceChannel = voiceChannels + .firstWhereOrNull((element) => element.id == channelId) as VoiceChannel?; + if (voiceChannel == null) { + return null; } - final selfMember = await guild.selfMember.getOrDownload(); - - if (selfMember.voiceState == null || selfMember.voiceState?.channel == null) { - guild.shard.changeVoiceState( - guild.id, - channelId, - selfDeafen: true, - ); - } + return voiceChannel.connectLavalink(); } -StringsCommandsEn getCommandTranslations(InteractionChatContext context) { +TranslationsCommandsEn getCommandTranslations(InteractionChatContext context) { final userLocale = context.interaction.locale ?? context.guild?.preferredLocale ?? - Locale.englishUs.code; + Locale.enUs; final commandTranslations = - AppLocaleUtils.parse(userLocale).translations.commands; + AppLocaleUtils.parse(userLocale.identifier).translations.commands; return commandTranslations; } -StringsCommandsEn getCommandTranslationsForGuild(IGuild guild) { +TranslationsCommandsEn getCommandTranslationsForGuild(Guild guild) { final userLocale = guild.preferredLocale; final commandTranslations = - AppLocaleUtils.parse(userLocale).translations.commands; + AppLocaleUtils.parse(userLocale.identifier).translations.commands; return commandTranslations; } -StringsCommandsEn getLocale(InteractionChatContext context) { +TranslationsCommandsEn getLocale(InteractionChatContext context) { final userLocale = context.interaction.locale ?? context.guild?.preferredLocale ?? - Locale.englishUs.code; + Locale.enUs; final commandTranslations = - AppLocaleUtils.parse(userLocale).translations.commands; + AppLocaleUtils.parse(userLocale.identifier).translations.commands; return commandTranslations; } diff --git a/lib/src/commands/info.dart b/lib/src/commands/info.dart index 1abc01f..3b97826 100644 --- a/lib/src/commands/info.dart +++ b/lib/src/commands/info.dart @@ -6,13 +6,12 @@ import 'dart:io'; -import 'package:get_it/get_it.dart'; -import 'package:logging/logging.dart'; +import 'package:injector/injector.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; -import 'package:nyxx_interactions/nyxx_interactions.dart'; +import 'package:nyxx_extensions/nyxx_extensions.dart'; +import 'package:nyxx_lavalink/nyxx_lavalink.dart'; import 'package:radio_horizon/radio_horizon.dart'; -import 'package:time_ago_provider/time_ago_provider.dart'; String getCurrentMemoryString() { final current = (ProcessInfo.currentRss / 1024 / 1024).toStringAsFixed(2); @@ -21,123 +20,112 @@ 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 { + id('info', (ChatContext context) async { context as InteractionChatContext; final commandTranslations = getCommandTranslations(context).info; - final nodes = _getIt.get().cluster.connectedNodes; - final players = nodes.values - .map((b) => b.players.length) - .reduce((value, element) => value + element); - final client = context.client as INyxxWebsocket; final color = getRandomColor(); + final currentUser = await context.client.user.get(); + final players = + await Injector.appInstance.get().listPlayers(); - _logger.info( - ''' -ChatCommand:info:call: { - 'guild': ${context.guild?.id ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', - ); + final startDate = Injector.appInstance.get().startDate; + final startDateStr = '${startDate.format(TimestampStyle.longDateTime)} ' + '(${startDate.format(TimestampStyle.relativeTime)})'; - final button = LinkButtonBuilder( - commandTranslations.addToServer, - client.app.getInviteUrl(), + final embed = EmbedBuilder( + color: color, + author: EmbedAuthorBuilder( + name: currentUser.username, + iconUrl: currentUser.avatar.url, + url: Uri.parse(githubUrl), + ), + footer: EmbedFooterBuilder( + text: 'nyxx ${ApiOptions.nyxxVersion}' + ' | Radio Horizon $packageVersion' + ' | Dart SDK ${getDartPlatform()}', + iconUrl: currentUser.avatar.url, + ), + fields: [ + EmbedFieldBuilder( + name: commandTranslations.cachedGuilds, + value: context.client.guilds.cache.length.toString(), + isInline: true, + ), + EmbedFieldBuilder( + name: commandTranslations.cachedUsers, + value: context.client.users.cache.length.toString(), + isInline: true, + ), + EmbedFieldBuilder( + name: commandTranslations.cachedChannels, + value: context.client.channels.cache.length.toString(), + isInline: true, + ), + EmbedFieldBuilder( + name: commandTranslations.currentPlayers, + value: players + .where((p) => p.state.isConnected && p.track != null) + .length + .toString(), + isInline: true, + ), + EmbedFieldBuilder( + name: commandTranslations.shardCount, + value: context.client.gateway.shards.length.toString(), + isInline: true, + ), + EmbedFieldBuilder( + name: commandTranslations.cachedMessages, + value: context.client.channels.cache.values + .whereType() + .map((c) => c.messages.cache.length) + .fold(0, (value, element) => value + element) + .toString(), + isInline: true, + ), + EmbedFieldBuilder( + name: commandTranslations.memoryUsage, + value: getCurrentMemoryString(), + isInline: true, + ), + EmbedFieldBuilder( + name: commandTranslations.uptime, + value: startDateStr, + isInline: false, + ), + ], ); - final gatewayLatency = (context.client as INyxxWebsocket) - .shardManager - .gatewayLatency - .inMilliseconds; - - final embed = EmbedBuilder() - ..color = color - ..addAuthor((author) { - author - ..name = client.self.tag - ..iconUrl = client.self.avatarUrl() - ..url = 'https://github.com/tomassasovsky/radio-horizon.dart'; - }) - ..addFooter((footer) { - footer.text = 'Radio Horizon' - ' | ${commandTranslations.shardOf( - index: context.guild?.shard.id ?? 0 + 1, - count: client.shards, - )}' - '${client.shards}' - ' | Dart SDK version ${Platform.version.split('(').first}'; - }) - ..addField( - name: commandTranslations.cachedGuilds, - content: context.client.guilds.length, - inline: true, - ) - ..addField( - name: commandTranslations.cachedUsers, - content: context.client.users.length, - inline: true, - ) - ..addField( - name: commandTranslations.cachedChannels, - content: context.client.channels.length, - inline: true, - ) - ..addField( - name: commandTranslations.cachedVoiceStates, - content: context.client.guilds.values - .map((g) => g.voiceStates.length) - .reduce((value, element) => value + element), - inline: true, - ) - ..addField( - name: commandTranslations.currentPlayers, - content: players, - inline: true, - ) - ..addField( - name: commandTranslations.shardCount, - content: client.shards, - inline: true, - ) - ..addField( - name: commandTranslations.cachedMessages, - content: context.client.channels.values - .whereType() - .map((c) => c.messageCache.length) - .reduce((value, element) => value + element), - inline: true, - ) - ..addField( - name: commandTranslations.memoryUsage, - content: getCurrentMemoryString(), - inline: true, - ) - ..addField( - name: commandTranslations.uptime, - content: formatFull(context.client.startTime), - inline: true, - ) - ..addField( - name: commandTranslations.gatewayLatency, - content: gatewayLatency, - inline: true, - ); + final inviteButton = ButtonBuilder.link( + url: context.client.application.getInviteUri( + scopes: ['bot', 'applications.commands'], + ), + label: commandTranslations.addToServer, + ); - final messageBuilder = ComponentMessageBuilder() - ..embeds = [embed] - ..addComponentRow(ComponentRowBuilder()..addComponent(button)); + final githubStarButton = ButtonBuilder.link( + url: Uri.parse(githubUrl), + label: commandTranslations.viewOnGithub, + ); - await context.respond(messageBuilder); + await context.respond( + MessageBuilder( + embeds: [embed], + components: [ + ActionRowBuilder( + components: [ + inviteButton, + githubStarButton, + ], + ), + ], + ), + ); }), localizedDescriptions: localizedValues( (translations) => translations.commands.info.description, diff --git a/lib/src/commands/music.dart b/lib/src/commands/music.dart index f0bc043..98faf28 100644 --- a/lib/src/commands/music.dart +++ b/lib/src/commands/music.dart @@ -7,20 +7,17 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:get_it/get_it.dart'; -import 'package:logging/logging.dart'; +import 'package:injector/injector.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; -import 'package:nyxx_interactions/nyxx_interactions.dart'; +import 'package:nyxx_lavalink/nyxx_lavalink.dart'; import 'package:radio_horizon/radio_horizon.dart'; import 'package:radio_horizon/src/checks.dart'; +import 'package:radio_horizon/src/helpers/music_queue.dart'; 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, _enMusicCommand.description, @@ -33,87 +30,20 @@ ChatGroup music = ChatGroup( ChatCommand( _enPlayCommand.command, _enPlayCommand.description, - id('music-play', ( - IChatContext context, - @Description('The name/url of the song/playlist to play') - @Autocomplete(autocompleteMusicQuery) - String query, - ) async { - context as InteractionChatContext; - final commandTranslations = - getCommandTranslations(context).music.children.play; - - _logger.info( - ''' -ChatCommand:music-play: { - 'query': $query, - 'guild': ${context.guild?.id.toString() ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', - ); - - final node = _getIt - .get() - .cluster - .getOrCreatePlayerNode(context.guild!.id); - await connectIfNeeded(context); - final result = await node.autoSearch(query); - - if (result.tracks.isEmpty) { - await context.respond( - MessageBuilder.content( - commandTranslations.noResults(query: query), - ), - ); - return; - } - - if (result.playlistInfo.name != null) { - for (final track in result.tracks) { - node - .play( - context.guild!.id, - track, - requester: context.member!.id, - channelId: context.channel.id, - ) - .queue(); - } - - await context.respond( - MessageBuilder.content( - commandTranslations.playlistEnqueued( - name: result.playlistInfo.name ?? '(Unknown)', - query: query, - ), - ), - ); - } else { - node - .play( - context.guild!.id, - result.tracks[0], - requester: context.member!.id, - channelId: context.channel.id, - ) - .queue(); - await context.respond( - MessageBuilder.content( - commandTranslations.songEnqueued( - title: result.tracks[0].info?.title ?? '(Unknown)', - query: query, - ), - ), - ); - } - - await _getIt - .get() - .deleteRadioFromList(context.guild!.id); - }), + id( + 'music-play', + ( + ChatContext context, + @Description('The name/url of the song/playlist to play') + @Autocomplete(autocompleteMusicQuery) + String query, + ) => + musicPlay( + context: context, + query: query, + source: 'ytsearch', + ), + ), localizedDescriptions: localizedValues( (translations) => translations.commands.music.children.play.description, ), @@ -121,6 +51,58 @@ ChatCommand:music-play: { (translations) => translations.commands.music.children.play.command, ), ), + ChatCommand( + 'ytplay', + 'Search for a song on YouTube', + id( + 'music-play-youtube', + ( + ChatContext context, + @Description('The name of the song to play') + @Autocomplete(autocompleteMusicYoutubeQuery) + String query, + ) => + musicPlay( + context: context, + query: query, + source: 'ytsearch', + ), + ), + localizedDescriptions: localizedValues( + (translations) => translations + .commands.music.children.play.children.youtube.description, + ), + localizedNames: localizedValues( + (translations) => + translations.commands.music.children.play.children.youtube.command, + ), + ), + ChatCommand( + 'dzplay', + 'Search for a song on Deezer', + id( + 'music-play-deezer', + ( + ChatContext context, + @Description('The name of the song to play') + @Autocomplete(autocompleteMusicDeezerQuery) + String query, + ) => + musicPlay( + context: context, + query: query, + source: 'dzsearch', + ), + ), + localizedDescriptions: localizedValues( + (translations) => translations + .commands.music.children.play.children.deezer.description, + ), + localizedNames: localizedValues( + (translations) => + translations.commands.music.children.play.children.deezer.command, + ), + ), ], localizedDescriptions: localizedValues( (translations) => translations.commands.music.description, @@ -130,34 +112,152 @@ ChatCommand:music-play: { ), ); -FutureOr?> autocompleteMusicQuery( - AutocompleteContext context, -) async { +FutureOr>?> + autocompleteMusicYoutubeQuery(AutocompleteContext context) async { + return autocompleteMusicQuery(context, sources: ['ytmsearch']); +} + +FutureOr>?> + autocompleteMusicDeezerQuery(AutocompleteContext context) async { + return autocompleteMusicQuery(context, sources: ['dzsearch']); +} + +FutureOr>?> autocompleteMusicQuery( + AutocompleteContext context, { + List sources = const ['ytmsearch', 'dzsearch'], +}) async { final query = context.currentValue; + if (query.isEmpty) { + return null; + } - _logger.info( - ''' -ChatCommand:music-play: autocompletion: { - 'query': $query, - 'guild': ${context.guild?.id.toString() ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', - ); - - final node = _getIt - .get() - .cluster - .getOrCreatePlayerNode(context.guild!.id); - final response = - await node.autoSearch(query).timeout(const Duration(milliseconds: 2500)); - - return response.tracks.map( - (e) => ArgChoiceBuilder( - e.info!.title.substring(0, math.min(e.info!.title.length, 100)), - e.info!.uri.substring(0, math.min(e.info!.uri.length, 100)), - ), - ); + final lavalinkClient = Injector.appInstance.get(); + final responses = []; + for (final source in sources) { + final response = await lavalinkClient.loadTrack('$source:$query'); + if (response is SearchLoadResult) { + responses.add(response); + } + } + + // Extract the data lists from responses + final tracksLists = responses.map((response) => response.data).toList(); + + // Interleave the tracks + final interleavedTracks = []; + final iterators = tracksLists.map((list) => list.iterator).toList(); + + var hasNext = true; + + while (hasNext && interleavedTracks.length < 25) { + hasNext = false; + for (final iterator in iterators) { + if (iterator.moveNext()) { + interleavedTracks.add(iterator.current); + hasNext = true; + if (interleavedTracks.length >= 25) break; + } + } + } + + // Convert the tracks to CommandOptionChoiceBuilder + final choices = interleavedTracks.map((track) { + final name = track.info.title; + final artist = track.info.author; + final title = '$name by $artist'; + final source = track.info.sourceName; + final value = track.info.uri.toString(); + + return CommandOptionChoiceBuilder( + name: + // ignore: lines_longer_than_80_chars + '${title.substring(0, math.min(title.length, 100 - source.length - 3))} ' + '($source)', + value: value.substring(0, math.min(value.length, 100)), + ); + }); + + return choices; +} + +Future musicPlay({ + required ChatContext context, + required String query, + required String source, +}) async { + context as InteractionChatContext; + + final commandTranslations = + getCommandTranslations(context).music.children.play; + + final player = await connectLavalink(context); + if (player == null) { + await context.respond( + MessageBuilder(content: commandTranslations.noResults(query: query)), + ); + return; + } + + LoadResult searchResult; + + final queryAsUri = Uri.tryParse(query); + final isUrl = queryAsUri != null && queryAsUri.hasScheme; + if (isUrl) { + searchResult = await player.lavalinkClient.loadTrack(query); + } else { + searchResult = await player.lavalinkClient.loadTrack('$source:$query'); + } + + if (searchResult is SearchLoadResult) { + if (searchResult.data.isEmpty) { + throw Exception('No tracks found'); + } + + trackQueues.getOrCreateQueue(player).queueTrack(searchResult.data.first); + + await context.respond( + MessageBuilder( + content: commandTranslations.songEnqueued( + title: searchResult.data.first.info.title, + query: query, + ), + ), + ); + } else if (searchResult is TrackLoadResult) { + trackQueues.getOrCreateQueue(player).queueTrack(searchResult.data); + + await context.respond( + MessageBuilder( + content: commandTranslations.songEnqueued( + title: searchResult.data.info.title, + query: query, + ), + ), + ); + } else if (searchResult is PlaylistLoadResult) { + if (searchResult.data.tracks.isEmpty) { + throw Exception('No tracks found'); + } + + final tracks = searchResult.data.tracks; + trackQueues.getOrCreateQueue(player).queueTracks(tracks); + + await context.respond( + MessageBuilder( + content: commandTranslations.playlistEnqueued( + name: searchResult.data.info.name, + query: query, + ), + ), + ); + } else { + throw Exception( + // ignore: lines_longer_than_80_chars, avoid_dynamic_calls + 'Unknown load result: $searchResult, ${searchResult.data.runtimeType}', + ); + } + + await Injector.appInstance + .get() + .deleteRadioFromList(context.guild!.id); } diff --git a/lib/src/commands/radio.dart b/lib/src/commands/radio.dart index dfb5233..7f0171f 100644 --- a/lib/src/commands/radio.dart +++ b/lib/src/commands/radio.dart @@ -8,10 +8,11 @@ import 'dart:async'; import 'dart:developer'; import 'dart:math' as math; -import 'package:logging/logging.dart'; +import 'package:injector/injector.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; -import 'package:nyxx_interactions/nyxx_interactions.dart'; +import 'package:nyxx_extensions/nyxx_extensions.dart'; +import 'package:nyxx_lavalink/nyxx_lavalink.dart'; import 'package:radio_browser_api/radio_browser_api.dart'; import 'package:radio_horizon/radio_horizon.dart'; import 'package:radio_horizon/src/checks.dart'; @@ -48,7 +49,7 @@ ChatGroup radio = ChatGroup( _enPlayCommand.command, _enPlayCommand.description, id('radioplay', ( - IChatContext context, + ChatContext context, @Description('The name of the Radio Station to play') @Autocomplete(autocompleteRadioQuery) String query, @@ -58,29 +59,12 @@ ChatGroup radio = ChatGroup( getCommandTranslations(context).radio.children.play; await context.respond( - MessageBuilder.content( - commandTranslations.searching(query: query), + MessageBuilder( + content: commandTranslations.searching(query: query), ), ); - _logger.info( - ''' -ChatCommand:radio-play: { - 'query': $query, - 'guild': ${context.guild?.id.toString() ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', - ); - - await connectIfNeeded(context, replace: true); - - final node = getIt - .get() - .cluster - .getOrCreatePlayerNode(context.guild!.id); + final player = await connectLavalink(context); late final RadioBrowserListResponse stations; @@ -93,12 +77,11 @@ ChatCommand:radio-play: { } if (stations.items.isEmpty) { - await context.respond( - MessageBuilder.content( - commandTranslations.noResults(query: query), + return context.respond( + MessageBuilder( + content: commandTranslations.noResults(query: query), ), ); - return; } final bestMatch = stations.items.first; @@ -106,32 +89,34 @@ ChatCommand:radio-play: { uuid: bestMatch.stationUUID, ); - final result = - await node.searchTracks(bestMatch.urlResolved ?? bestMatch.url); - if (result.tracks.isEmpty) { - await context.respond( - MessageBuilder.content( - commandTranslations.noResults(query: query), + final lavalinkClient = Injector.appInstance.get(); + + final result = await lavalinkClient + .loadTrack(bestMatch.urlResolved ?? bestMatch.url); + if (result is! TrackLoadResult) { + return context.respond( + MessageBuilder( + content: commandTranslations.noResults(query: query), ), ); - return; } - final track = result.tracks.first; - node - ..players[context.guild!.id]!.queue.clear() - ..play( - context.guild!.id, - track, - replace: true, - requester: context.member!.id, - channelId: context.channel.id, - ).startPlaying(); - - final databaseService = getIt.get(); + await context.respond( + MessageBuilder( + content: commandTranslations.stationEnqueued( + name: result.data.info.title, + query: query, + ), + ), + ); + + final track = result.data; + await player?.play(track); + + final databaseService = Injector.appInstance.get(); await databaseService.setCurrentRadio( context.guild!.id, - context.member!.voiceState!.channel!.id, + context.guild!.voiceStates[context.member!.id]!.channelId!, context.channel.id, bestMatch, ); @@ -141,10 +126,10 @@ ChatCommand:radio-play: { ..title = commandTranslations.startedPlaying ..description = commandTranslations.startedPlayingDescription( radio: bestMatch.name, - mention: context.member?.mention ?? '(Unknown)', + mention: context.member?.user?.mention ?? '(Unknown)', ); - await context.respond(MessageBuilder.embed(embed)); + await context.respond(MessageBuilder(embeds: [embed])); }), localizedDescriptions: localizedValues( (translations) => translations.commands.radio.children.play.description, @@ -157,15 +142,15 @@ ChatCommand:radio-play: { _enPlayRandomCommand.command, _enPlayRandomCommand.description, id('radio-play-random', ( - IChatContext context, + ChatContext context, ) async { context as InteractionChatContext; final commandTranslations = getCommandTranslations(context).radio.children.playRandom; await context.respond( - MessageBuilder.content( - commandTranslations.searching, + MessageBuilder( + content: commandTranslations.searching, ), ); @@ -177,65 +162,38 @@ ChatCommand:radio-play: { final randomIndex = math.Random().nextInt(radios.items.length); final radio = radios.items[randomIndex]; - _logger.info( - ''' -ChatCommand:radio-play-random: { - 'guild': ${context.guild?.id.toString() ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', - ); - - await connectIfNeeded(context, replace: true); + final lavalinkClient = Injector.appInstance.get(); + final player = await connectLavalink(context); + await _radioBrowserClient.clickStation(uuid: radio.stationUUID); - final node = getIt - .get() - .cluster - .getOrCreatePlayerNode(context.guild!.id); - - await _radioBrowserClient.clickStation( - uuid: radio.stationUUID, - ); - - final result = await node.searchTracks(radio.urlResolved ?? radio.url); - if (result.tracks.isEmpty) { - await context.respond( - MessageBuilder.content( - commandTranslations.errors.noResults, - ), + final result = + await lavalinkClient.loadTrack(radio.urlResolved ?? radio.url); + if (result is! TrackLoadResult) { + return context.respond( + MessageBuilder(content: commandTranslations.errors.noResults), ); - return; } - final track = result.tracks.first; - node - ..players[context.guild!.id]!.queue.clear() - ..play( - context.guild!.id, - track, - replace: true, - requester: context.member!.id, - channelId: context.channel.id, - ).startPlaying(); - - await getIt.get().setCurrentRadio( - context.guild!.id, - context.member!.voiceState!.channel!.id, - context.channel.id, - radio, - ); + final track = result.data; + await player?.play(track); + + final databaseService = Injector.appInstance.get(); + await databaseService.setCurrentRadio( + context.guild!.id, + context.guild!.voiceStates[context.member!.id]!.channelId!, + context.channel.id, + radio, + ); final embed = EmbedBuilder() ..color = getRandomColor() ..title = commandTranslations.startedPlaying ..description = commandTranslations.startedPlayingDescription( radio: radio.name, - mention: context.member?.mention ?? '(Unknown)', + mention: context.member?.user?.mention ?? '(Unknown)', ); - await context.respond(MessageBuilder.embed(embed)); + await context.respond(MessageBuilder(embeds: [embed])); }), localizedDescriptions: localizedValues( (translations) => @@ -250,7 +208,7 @@ ChatCommand:radio-play-random: { _enRecognizeCommand.command, _enRecognizeCommand.description, id('radio-recognize', ( - IChatContext context, + ChatContext context, ) async { context as InteractionChatContext; final translations = getCommandTranslations(context); @@ -258,8 +216,10 @@ ChatCommand:radio-play-random: { CurrentStationInfo? stationInfo; try { - final recognitionService = getIt.get(); - final databaseService = getIt.get(); + final databaseService = Injector.appInstance.get(); + final recognitionService = + Injector.appInstance.get(); + final guildId = context.guild!.id; var recognitionSampleDuration = 10; @@ -285,10 +245,13 @@ ChatCommand:radio-play-random: { if (result == null) { await context.respond( - MessageBuilder.embed( - EmbedBuilder() - ..color = DiscordColor.red - ..title = commandTranslations.errors.noResults, + MessageBuilder( + embeds: [ + EmbedBuilder( + color: const DiscordColor.fromRgb(255, 0, 0), + title: commandTranslations.errors.noResults, + ), + ], ), ); return null; @@ -298,39 +261,54 @@ ChatCommand:radio-play-random: { CurrentStationInfo.fromShazamResult(result!, guildRadio); } catch (e) { await context.respond( - MessageBuilder.embed( - EmbedBuilder() - ..color = DiscordColor.red - ..title = commandTranslations.errors.noResults, + MessageBuilder( + embeds: [ + EmbedBuilder( + color: const DiscordColor.fromRgb(255, 0, 0), + title: commandTranslations.errors.noResults, + ), + ], ), ); return null; } final color = getRandomColor(); - - final embed = EmbedBuilder() - ..color = color - ..title = stationInfo.title - ..description = commandTranslations.requestedBy( - mention: context.member?.mention ?? '(Unknown)', - ) - ..addField( - name: commandTranslations.radioStationField, - content: stationInfo.name, - ) - ..thumbnailUrl = stationInfo.image - ..url = stationInfo.url; - final genre = stationInfo.genre; - if (genre != null) { - embed.addField( - name: commandTranslations.genreField, - content: genre, - ); - } - await context.respond(MessageBuilder.embed(embed)); + final embed = EmbedBuilder( + color: color, + title: stationInfo.title, + description: commandTranslations.requestedBy( + mention: context.member?.user?.mention ?? '(Unknown)', + ), + thumbnail: stationInfo.image != null + ? EmbedThumbnailBuilder(url: Uri.parse(stationInfo.image!)) + : null, + url: stationInfo.url != null ? Uri.parse(stationInfo.url!) : null, + fields: [ + if (stationInfo.name != null) + EmbedFieldBuilder( + name: commandTranslations.radioStationField, + value: stationInfo.name!, + isInline: true, + ), + if (stationInfo.image != null && stationInfo.url != null) + EmbedFieldBuilder( + name: stationInfo.image!, + value: stationInfo.url!, + isInline: true, + ), + if (genre != null) + EmbedFieldBuilder( + name: commandTranslations.genreField, + value: genre, + isInline: true, + ), + ], + ); + + await context.respond(MessageBuilder(embeds: [embed])); } catch (e, stacktrace) { _logger.severe( 'Failed to recognize radio', @@ -339,12 +317,15 @@ ChatCommand:radio-play-random: { ); await context.respond( - MessageBuilder.embed( - EmbedBuilder() - ..color = DiscordColor.red - ..title = commandTranslations.errors.title - ..description = - handleRecognitionExceptions(e, stacktrace, translations), + MessageBuilder( + embeds: [ + EmbedBuilder( + color: const DiscordColor.fromRgb(255, 0, 0), + title: commandTranslations.errors.title, + description: + handleRecognitionExceptions(e, stacktrace, translations), + ), + ], ), ); } @@ -362,7 +343,7 @@ ChatCommand:radio-play-random: { _enUpvoteCommand.command, _enUpvoteCommand.description, id('radio-upvote', ( - IChatContext context, + ChatContext context, ) async { context as InteractionChatContext; final translations = getCommandTranslations(context); @@ -370,15 +351,18 @@ ChatCommand:radio-play-random: { late GuildRadio? guildRadio; try { - guildRadio = await getIt + guildRadio = await Injector.appInstance .get() .currentRadio(context.guild!.id); } on RadioNotPlayingException { await context.respond( - MessageBuilder.embed( - EmbedBuilder() - ..color = DiscordColor.red - ..title = commandTranslations.errors.noRadioPlaying, + MessageBuilder( + embeds: [ + EmbedBuilder( + color: const DiscordColor.fromRgb(255, 0, 0), + title: commandTranslations.errors.noRadioPlaying, + ), + ], ), ); return; @@ -395,7 +379,7 @@ ChatCommand:radio-play-random: { radio: guildRadio.station.name, ); - await context.respond(MessageBuilder.embed(embed)); + await context.respond(MessageBuilder(embeds: [embed])); }), ), ], @@ -407,7 +391,7 @@ ChatCommand:radio-play-random: { ), ); -FutureOr?> autocompleteRadioQuery( +FutureOr>?> autocompleteRadioQuery( AutocompleteContext context, ) async { final query = context.currentValue; @@ -434,8 +418,8 @@ FutureOr?> autocompleteRadioQuery( final croppedName = e.name.substring(0, math.min(e.name.length, 58)).trim(); - return ArgChoiceBuilder( - e.name.substring(0, math.min(e.name.length, 100)), + return CommandOptionChoiceBuilder( + name: e.name.substring(0, math.min(e.name.length, 100)), // limit the name's length to 59 characters, because Discord has a limit // of 100 characters for the choice's and value. // 59 is due to the fact that the station's UUID is also added to the @@ -443,8 +427,8 @@ FutureOr?> autocompleteRadioQuery( // around the UUID + pre-text space). // // The value is used to identify the station when the user selects it. - '${e.name.length >= 58 ? '$croppedName...' : croppedName} ' - '(${e.stationUUID})', + value: '${e.name.length >= 58 ? '$croppedName...' : croppedName} ' + '(${e.stationUUID})', ); }, ); @@ -453,7 +437,7 @@ FutureOr?> autocompleteRadioQuery( String handleRecognitionExceptions( Object e, StackTrace stackTrace, - StringsCommandsEn commandTranslations, + TranslationsCommandsEn commandTranslations, ) { log('Exception: ', error: e, stackTrace: stackTrace); final errors = commandTranslations.radio.children.recognize.errors; diff --git a/lib/src/commands/sound.dart b/lib/src/commands/sound.dart index 9697e39..d0d246f 100644 --- a/lib/src/commands/sound.dart +++ b/lib/src/commands/sound.dart @@ -1,8 +1,9 @@ -import 'package:logging/logging.dart'; +import 'package:injector/injector.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; import 'package:radio_horizon/radio_horizon.dart'; import 'package:radio_horizon/src/checks.dart'; +import 'package:radio_horizon/src/helpers/music_queue.dart'; final _enSkipCommand = AppLocale.en.translations.commands.skip; final _enStopCommand = AppLocale.en.translations.commands.stop; @@ -12,44 +13,45 @@ final _enPauseCommand = AppLocale.en.translations.commands.pause; final _enResumeCommand = AppLocale.en.translations.commands.resume; final _enVolumeCommand = AppLocale.en.translations.commands.volume; -final _skipLogger = Logger('command/skip'); - final skip = ChatCommand( _enSkipCommand.command, _enSkipCommand.description, - checks: [connectedToAVoiceChannelCheck], - id('skip', (IChatContext context) async { + checks: [botConnectedToAVoiceChannelCheck], + id('skip', (ChatContext context) async { context as InteractionChatContext; final commandTranslations = getCommandTranslations(context).skip; + final playCommandTranslations = + getCommandTranslations(context).music.children.play; - _skipLogger.info( - ''' -ChatCommand:skip: { - 'guild': ${context.guild?.id.toString() ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', - ); + final player = await connectLavalink(context); + if (player == null) { + await context + .respond(MessageBuilder(content: commandTranslations.nothingPlaying)); + return; + } - final node = getIt - .get() - .cluster - .getOrCreatePlayerNode(context.guild!.id); - final player = node.players[context.guild!.id]!; + final queue = trackQueues.getOrCreateQueue(player); + if (queue.isEmpty) { + await context + .respond(MessageBuilder(content: commandTranslations.nothingPlaying)); + return; + } + + final next = queue.skip(); - if (player.queue.isEmpty) { + if (next != null) { await context.respond( - MessageBuilder.content(commandTranslations.nothingPlaying), + MessageBuilder( + content: playCommandTranslations.songEnqueued( + title: next.info.title, + query: 'from queue', + ), + ), ); - return; + } else { + await context + .respond(MessageBuilder(content: commandTranslations.skipped)); } - - node.skip(context.guild!.id); - await context.respond( - MessageBuilder.content(commandTranslations.skipped), - ); }), localizedDescriptions: localizedValues( (translations) => translations.commands.skip.description, @@ -59,34 +61,32 @@ ChatCommand:skip: { ), ); -final _leaveLogger = Logger('command/leave'); - final leave = ChatCommand( _enLeaveCommand.command, _enLeaveCommand.description, - checks: [connectedToAVoiceChannelCheck], - id('leave', (IChatContext context) async { + checks: [botConnectedToAVoiceChannelCheck], + id('leave', (ChatContext context) async { context as InteractionChatContext; final commandTranslations = getCommandTranslations(context).leave; - _leaveLogger.info( - ''' -ChatCommand:leave: { - 'guild': ${context.guild?.id.toString() ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', + final nyxxGateway = Injector.appInstance.get(); + + final player = await connectLavalink(context); + if (player != null) { + final queue = trackQueues.getOrCreateQueue(player); + queue.clear(); + } + + nyxxGateway.updateVoiceState( + context.guild!.id, + GatewayVoiceStateBuilder( + channelId: null, + isMuted: false, + isDeafened: false, + ), ); - getIt - .get() - .cluster - .getOrCreatePlayerNode(context.guild!.id) - .destroy(context.guild!.id); - context.guild!.shard.changeVoiceState(context.guild!.id, null); - await context.respond(MessageBuilder.content(commandTranslations.left)); + await context.respond(MessageBuilder(content: commandTranslations.left)); }), localizedDescriptions: localizedValues( (translations) => translations.commands.leave.description, @@ -96,47 +96,30 @@ ChatCommand:leave: { ), ); -final _joinLogger = Logger('command/join'); - final join = ChatCommand( _enJoinCommand.command, _enJoinCommand.description, - checks: [notConnectedToAVoiceChannelCheck], - id('join', (IChatContext context) async { + checks: [botNotConnectedToAVoiceChannelCheck], + id('join', (ChatContext context) async { context as InteractionChatContext; final commandTranslations = getCommandTranslations(context).join; - _joinLogger.info( - ''' -ChatCommand:join: { - 'guild': ${context.guild?.id.toString() ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', - ); + await connectLavalink(context); - getIt.get().cluster.getOrCreatePlayerNode(context.guild!.id); - await connectIfNeeded(context); - await context.respond(MessageBuilder.content(commandTranslations.joined)); + await context.respond(MessageBuilder(content: commandTranslations.joined)); }), - localizedDescriptions: localizedValues( - (translations) => translations.commands.join.description, - ), - localizedNames: localizedValues( - (translations) => translations.commands.join.command, - ), + localizedDescriptions: + localizedValues((translations) => translations.commands.join.description), + localizedNames: + localizedValues((translations) => translations.commands.join.command), ); -final _volumeLogger = Logger('command/volume'); - final volume = ChatCommand( _enVolumeCommand.command, _enVolumeCommand.description, - checks: [connectedToAVoiceChannelCheck], + checks: [botConnectedToAVoiceChannelCheck], id('volume', ( - IChatContext context, + ChatContext context, @Description( 'The new volume, this value must be contained between 0 and 1000', ) @@ -146,67 +129,30 @@ final volume = ChatCommand( context as InteractionChatContext; final commandTranslations = getCommandTranslations(context).volume; - _volumeLogger.info( - ''' -ChatCommand:volume: { - 'volume': $volume, - 'guild': ${context.guild?.id.toString() ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', - ); - - getIt - .get() - .cluster - .getOrCreatePlayerNode(context.guild!.id) - .volume( - context.guild!.id, - volume, - ); + final player = await connectLavalink(context); + await player?.setVolume(volume); await context.respond( - MessageBuilder.content( - commandTranslations.volumeSet(volume: volume), + MessageBuilder( + content: commandTranslations.volumeSet(volume: volume), ), ); }), - localizedDescriptions: localizedValues( - (translations) => translations.commands.volume.description, - ), - localizedNames: localizedValues( - (translations) => translations.commands.volume.command, - ), + localizedDescriptions: localizedValues((t) => t.commands.volume.description), + localizedNames: localizedValues((t) => t.commands.volume.command), ); -final _pauseLogger = Logger('command/pause'); - final pause = ChatCommand( _enPauseCommand.command, _enPauseCommand.description, - id('pause', (IChatContext context) async { + id('pause', (ChatContext context) async { context as InteractionChatContext; final commandTranslations = getCommandTranslations(context).pause; - _pauseLogger.info( - ''' -ChatCommand:pause: { - 'guild': ${context.guild?.id.toString() ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', - ); + final player = await connectLavalink(context); + await player?.pause(); - getIt - .get() - .cluster - .getOrCreatePlayerNode(context.guild!.id) - .pause(context.guild!.id); - await context.respond(MessageBuilder.content(commandTranslations.paused)); + await context.respond(MessageBuilder(content: commandTranslations.paused)); }), localizedDescriptions: localizedValues( (translations) => translations.commands.pause.description, @@ -216,32 +162,17 @@ ChatCommand:pause: { ), ); -final _resumeLogger = Logger('command/resume'); - final resume = ChatCommand( _enResumeCommand.command, _enResumeCommand.description, - id('resume', (IChatContext context) async { + id('resume', (ChatContext context) async { context as InteractionChatContext; final commandTranslations = getCommandTranslations(context).resume; - _resumeLogger.info( - ''' -ChatCommand:resume: { - 'guild': ${context.guild?.id.toString() ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', - ); + final player = await connectLavalink(context); + await player?.resume(); - getIt - .get() - .cluster - .getOrCreatePlayerNode(context.guild!.id) - .resume(context.guild!.id); - await context.respond(MessageBuilder.content(commandTranslations.resumed)); + await context.respond(MessageBuilder(content: commandTranslations.resumed)); }), localizedDescriptions: localizedValues( (translations) => translations.commands.resume.description, @@ -254,28 +185,19 @@ ChatCommand:resume: { final stop = ChatCommand( _enStopCommand.command, _enStopCommand.description, - checks: [connectedToAVoiceChannelCheck], - id('stop', (IChatContext context) async { + checks: [botConnectedToAVoiceChannelCheck], + id('stop', (ChatContext context) async { context as InteractionChatContext; final commandTranslations = getCommandTranslations(context).stop; - _resumeLogger.info( - ''' -ChatCommand:stop: { - 'guild': ${context.guild?.id.toString() ?? 'null'}, - 'guild_name': ${context.guild?.name ?? 'null'}, - 'guild_preferred_locale': ${context.guild?.preferredLocale ?? 'null'}, - 'channel': ${context.channel.id}, - 'user': ${context.member?.id.toString() ?? 'null'}, -}''', - ); + final player = await connectLavalink(context); + + if (player != null) { + final queue = trackQueues.getOrCreateQueue(player); + queue.clear(); + } - getIt - .get() - .cluster - .getOrCreatePlayerNode(context.guild!.id) - .stop(context.guild!.id); - await context.respond(MessageBuilder.content(commandTranslations.stopped)); + await context.respond(MessageBuilder(content: commandTranslations.stopped)); }), localizedDescriptions: localizedValues( (translations) => translations.commands.stop.description, diff --git a/lib/src/dotenv.dart b/lib/src/dotenv.dart index 108b932..db37334 100644 --- a/lib/src/dotenv.dart +++ b/lib/src/dotenv.dart @@ -25,5 +25,5 @@ enum DotEnvFlavour { DotEnv get dotenv => _dotEnv; } -var _dotEnv = DotEnv(includePlatformEnvironment: true); +var _dotEnv = DotEnv(includePlatformEnvironment: true, quiet: true); late DotEnvFlavour dotEnvFlavour; diff --git a/lib/src/error_handler.dart b/lib/src/error_handler.dart index d7759fc..fb474ad 100644 --- a/lib/src/error_handler.dart +++ b/lib/src/error_handler.dart @@ -4,10 +4,8 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -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:radio_horizon/radio_horizon.dart'; final _logger = Logger('ROD.CommandErrors'); @@ -16,7 +14,7 @@ Future commandErrorHandler(CommandsException error) async { if (error is CommandInvocationException) { final context = error.context; - final locale = context.guild?.preferredLocale ?? Locale.englishUs.code; + final locale = (context.guild?.preferredLocale ?? Locale.enUs).identifier; final translations = AppLocaleUtils.parse(locale).translations; String? title; @@ -27,26 +25,26 @@ Future commandErrorHandler(CommandsException error) async { switch (error.failed.name) { case 'musicConnectedToVC': await context.respond( - MessageBuilder.content( - translations.errorHandler.musicConnectedToVC, + MessageBuilder( + content: translations.errorHandler.musicConnectedToVC, ), ); case 'musicNotConnectedToVC': await context.respond( - MessageBuilder.content( - translations.errorHandler.musicNotConnectedToVC, + MessageBuilder( + content: translations.errorHandler.musicNotConnectedToVC, ), ); case 'musicSameVC': await context.respond( - MessageBuilder.content( - translations.errorHandler.musicSameVC, + MessageBuilder( + content: translations.errorHandler.musicSameVC, ), ); case 'musicUserConnectedToVC': await context.respond( - MessageBuilder.content( - translations.errorHandler.musicUserConnectedToVC, + MessageBuilder( + content: translations.errorHandler.musicUserConnectedToVC, ), ); default: @@ -79,16 +77,13 @@ Future commandErrorHandler(CommandsException error) async { // Send a generic "an error occurred" response final embed = EmbedBuilder() - ..color = DiscordColor.red + ..color = const DiscordColor.fromRgb(255, 0, 0) ..title = title ?? translations.errorHandler.title ..description = description ?? translations.errorHandler.fallbackDescription - ..addFooter((footer) { - footer.text = error.runtimeType.toString(); - }) ..timestamp = DateTime.now(); - await context.respond(MessageBuilder.embed(embed)); + await context.respond(MessageBuilder(embeds: [embed])); return; } diff --git a/lib/src/helpers/music_queue.dart b/lib/src/helpers/music_queue.dart new file mode 100644 index 0000000..e9f09f8 --- /dev/null +++ b/lib/src/helpers/music_queue.dart @@ -0,0 +1,65 @@ +import 'package:nyxx/nyxx.dart'; +import 'package:nyxx_lavalink/nyxx_lavalink.dart'; + +final trackQueues = {}; + +class MusicQueue { + MusicQueue(this.player) { + player.onTrackEnd.listen((event) { + _playNext(); + }); + } + + final LavalinkPlayer player; + final List _queue = []; + bool _isPlaying = false; + + void queueTrack(Track track) { + _queue.add(track); + if (!_isPlaying) { + _playNext(); + } + } + + void queueTracks(List tracks) { + _queue.addAll(tracks); + if (!_isPlaying) { + _playNext(); + } + } + + void clear() { + _queue.clear(); + player.stopPlaying(); + _isPlaying = false; + } + + /// Skip the current track and play the next one. + /// Returns the next track that will be played. + Track? skip() { + // this will trigger the onTrackEnd event + final nextTrack = _queue.elementAtOrNull(0); + player.stopPlaying(); + return nextTrack; + } + + void _playNext() { + if (_queue.isNotEmpty) { + final nextTrack = _queue.removeAt(0); + player.play(nextTrack); + _isPlaying = true; + } else { + player.stopPlaying(); + _isPlaying = false; + } + } + + bool get isEmpty => _queue.isEmpty; + bool get isPlaying => _isPlaying; +} + +extension QueueHelper on Map { + MusicQueue getOrCreateQueue(LavalinkPlayer player) { + return this[player.guildId] ??= MusicQueue(player); + } +} diff --git a/lib/src/models/exceptions.dart b/lib/src/models/exceptions.dart new file mode 100644 index 0000000..7f37b69 --- /dev/null +++ b/lib/src/models/exceptions.dart @@ -0,0 +1,3 @@ +class UserNotConnectedToVoiceChannelException implements Exception { + const UserNotConnectedToVoiceChannelException(); +} diff --git a/lib/src/models/music_links_response/music_links_response.dart b/lib/src/models/music_links_response/music_links_response.dart index c97eca6..6dfbe77 100644 --- a/lib/src/models/music_links_response/music_links_response.dart +++ b/lib/src/models/music_links_response/music_links_response.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:json_annotation/json_annotation.dart'; -import 'package:nyxx_interactions/nyxx_interactions.dart'; import 'package:radio_horizon/radio_horizon.dart'; export 'platform.dart'; @@ -51,34 +50,4 @@ class MusicLinksResponse { return localPlatforms; } - - List get componentRows { - var addedLinks = 0; - final buttonRowBuilders = []; - - void addLinkButton(MusicLinksPlatform? platform) { - if (platform == null) return; - - final url = platform.url; - final label = platform.name; - - if (url == null || label == null) return; - - // the buttons in a button row can't be more than 5 - if (addedLinks % 5 == 0) { - buttonRowBuilders.add(ComponentRowBuilder()); - } - - final button = LinkButtonBuilder(label, url); - buttonRowBuilders.last.addComponent(button); - addedLinks++; - } - - for (final element in uniquePlatforms) { - addLinkButton(element); - } - - if (addedLinks == 0) return []; - return buttonRowBuilders; - } } diff --git a/lib/src/models/song_recognition/guild_radio.dart b/lib/src/models/song_recognition/guild_radio.dart index eca67eb..2de813d 100644 --- a/lib/src/models/song_recognition/guild_radio.dart +++ b/lib/src/models/song_recognition/guild_radio.dart @@ -18,10 +18,10 @@ class GuildRadio { factory GuildRadio.fromJson(Map json) { json = json.cast(); return GuildRadio( - Snowflake(json['guildId'].toString()), + Snowflake.parse(json['guildId'].toString()), station: Station.fromJson((json['station'] as Map).cast()), - voiceChannelId: Snowflake(json['voiceChannelId'].toString()), - textChannelId: Snowflake(json['textChannelId'].toString()), + voiceChannelId: Snowflake.parse(json['voiceChannelId'].toString()), + textChannelId: Snowflake.parse(json['textChannelId'].toString()), ); } diff --git a/lib/src/requires_initialization.dart b/lib/src/requires_initialization.dart new file mode 100644 index 0000000..c500aed --- /dev/null +++ b/lib/src/requires_initialization.dart @@ -0,0 +1,3 @@ +mixin RequiresInitialization { + Future init(); +} diff --git a/lib/src/services/bootup.dart b/lib/src/services/bootup.dart index adcda49..1170175 100644 --- a/lib/src/services/bootup.dart +++ b/lib/src/services/bootup.dart @@ -1,39 +1,19 @@ import 'dart:async'; -import 'package:logging/logging.dart'; +import 'package:injector/injector.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_lavalink/nyxx_lavalink.dart'; import 'package:radio_horizon/radio_horizon.dart'; -import 'package:retry/retry.dart'; class BootUpService { - BootUpService({ - required INyxxWebsocket client, - required this.databaseService, - }) : _client = client; + BootUpService(); - final DatabaseService databaseService; - final INyxxWebsocket _client; + DatabaseService get databaseService => Injector().get(); + NyxxGateway get _client => Injector().get(); /// Grabs the previously playing radio and sets it as the current one /// for the guild - Future initialize(ICluster cluster) async { - final mCluster = await retry( - () async { - if (cluster.connectedNodes.isEmpty) { - throw Exception('No nodes connected yet... retrying'); - } else { - return cluster; - } - }, - maxAttempts: 10, - delayFactor: const Duration(seconds: 5), - ); - - if (mCluster == null || mCluster.connectedNodes.isEmpty) { - return; - } - + Future init() async { final allPlaying = await databaseService.getAllPlaying(); if (allPlaying == null) { @@ -42,7 +22,7 @@ class BootUpService { for (final playing in allPlaying) { try { - final guild = await _client.fetchGuild(playing.guildId); + final guild = await _client.guilds.fetch(playing.guildId); await connectToChannel( guild, @@ -50,17 +30,15 @@ class BootUpService { replace: true, ); - 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(); + final voiceState = guild.voiceStates[playing.voiceChannelId]!; + final voiceChannel = + (await voiceState.channel!.fetch()) as VoiceChannel; + + final lavalinkPlayer = await voiceChannel.connectLavalink(); + + await lavalinkPlayer.stopPlaying(); + await lavalinkPlayer + .playEncoded(playing.station.urlResolved ?? playing.station.url); await databaseService.setPlaying( GuildRadio( diff --git a/lib/src/services/bot_start_duration.dart b/lib/src/services/bot_start_duration.dart new file mode 100644 index 0000000..675722d --- /dev/null +++ b/lib/src/services/bot_start_duration.dart @@ -0,0 +1,10 @@ +import 'package:radio_horizon/src/requires_initialization.dart'; + +class BotStartDuration implements RequiresInitialization { + late final DateTime startDate; + + @override + Future init() async { + startDate = DateTime.now(); + } +} diff --git a/lib/src/services/db.dart b/lib/src/services/db.dart index a72f142..4a7d72f 100644 --- a/lib/src/services/db.dart +++ b/lib/src/services/db.dart @@ -1,27 +1,18 @@ import 'dart:async'; -import 'package:logging/logging.dart'; +import 'package:injector/injector.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 { - DatabaseService(this._client) { - _client.onReady.listen((_) async { - await _addServer(); - }); - } + DatabaseService(); late Db _db; + NyxxGateway get _client => Injector().get(); - Future _checkConnection() async { - if (!_db.isConnected) { - await _db.open(); - } - } - - Future initialize() async { + Future init() async { /// Connects to the MongoDB database _db = await Db.create(mongoDBConnection); await _db.open(); @@ -35,13 +26,15 @@ class DatabaseService { key: 'guildId', unique: true, ); - } - Future _addServer() async { - _client.eventsWs.onGuildCreate.listen((event) async { + _client.onEvent.listen((event) async { + if (event is! GuildCreateEvent) { + return; + } + try { - await _checkConnection(); - await serverCollection.insert({'guildId': event.guild.id.id}); + final guild = await event.guild.get(); + await serverCollection.insert({'guildId': guild.id.value}); } catch (e, stackTrace) { Logger('DB').warning('Error while adding server to db', e, stackTrace); } @@ -50,7 +43,6 @@ class DatabaseService { Future setPlaying(GuildRadio guildRadio) async { try { - await _checkConnection(); await radioPlayingCollection.insert(guildRadio.toJson()); } catch (e, stackTrace) { Logger('DB').warning('Error while setting playing to db', e, stackTrace); @@ -95,7 +87,6 @@ class DatabaseService { Future getPlaying(Snowflake guildId) async { try { - await _checkConnection(); final res = await radioPlayingCollection.findOne( where.eq('guildId', guildId.toString()), ); @@ -118,7 +109,6 @@ class DatabaseService { Future?> getAllPlaying() async { try { - await _checkConnection(); final res = await radioPlayingCollection.find().toList(); if (res.isEmpty) { @@ -139,7 +129,6 @@ class DatabaseService { Future setNotPlaying(Snowflake guildId) async { try { - await _checkConnection(); await radioPlayingCollection.remove( where.eq('guildId', guildId.toString()), ); @@ -155,7 +144,5 @@ class DatabaseService { DbCollection get serverCollection => _db.collection('servers'); DbCollection get radioPlayingCollection => _db.collection('radioPlaying'); - final INyxxWebsocket _client; - FutureOr close() async => _db.close(); } diff --git a/lib/src/services/music.dart b/lib/src/services/music.dart deleted file mode 100644 index 0ed8258..0000000 --- a/lib/src/services/music.dart +++ /dev/null @@ -1,329 +0,0 @@ -// Copyright (c) 2022, Tomás Sasovsky -// -// Use of this source code is governed by an MIT-style -// 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); - - final logger = Logger('MusicService'); - final INyxxWebsocket _client; - - ICluster get cluster => - _cluster ?? - (throw Exception('Cluster must be accessed after `on_ready` event')); - - /// Initializes the music service - Future initialize() 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)', - ); - - 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; - } - - 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)', - ); - - return; - } - - logger.info( - 'MusicService:constructor: lavalink-connection-successful', - ); - - 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; - - Future _trackStarted(ITrackStartEvent event) async { - final player = event.node.players[event.guildId]; - - if (player == null) { - logger.severe( - 'Received track start event for guild ${event.guildId} but ' - 'no player was found', - ); - - return; - } - - logger.info( - 'Track started: ${player.nowPlaying?.track.info?.title} ' - '(${player.nowPlaying?.track.info?.uri})', - ); - - if (player.nowPlaying != null) { - final track = player.nowPlaying; - - final log = StringBuffer() - ..writeln('Track started: ${track?.track.info?.title}') - ..writeln('Track URL: ${track?.track.info?.uri}') - ..writeln('Parameters: ${queuedTrackToAnalyticsParameters(track!)}'); - - logger.info(log.toString()); - - final embed = EmbedBuilder() - ..color = getRandomColor() - ..title = _enMusicService.trackStarted.title - ..description = _enMusicService.trackStarted.description( - track: track.track.info!.title, - uri: track.track.info!.uri, - requester: track.requester!, - ) - ..thumbnailUrl = - 'https://img.youtube.com/vi/${track.track.info?.identifier}/hqdefault.jpg'; - - await _client.httpEndpoints.sendMessage( - track.channelId!, - MessageBuilder.embed(embed), - ); - } - } - - Future _trackEnded(ITrackEndEvent event) async { - await Future.delayed(const Duration(seconds: 30)); - - if (!event.node.players.containsKey(event.guildId)) { - 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) { - final guildId = event.guildId; - final hasGuild = event.client.guilds.containsKey(guildId); - - if (!hasGuild) { - return; - } - - final guild = await event.client.httpEndpoints.fetchGuild(guildId); - - event.node.destroy(guild.id); - guild.shard.changeVoiceState(guild.id, null); - - // delete the current radio station from the list, if it exists - await getIt.get().deleteRadioFromList(guild.id); - - logger.info( - 'Disconnected from voice channel in guild ${guild.id} ' - '(${guild.name})', - ); - } - } - - Future _trackStuck(ITrackStuckEvent event) async { - final player = event.node.players[event.guildId]; - - if (player == null) { - logger.severe( - 'Received track stuck event for guild ${event.guildId} but no player ' - 'was found', - ); - - return; - } - - if (player.queue.isNotEmpty) { - final track = player.queue[0]; - - logger.severe( - 'MusicService:_trackStuck: ${queuedTrackToAnalyticsParameters(track)}', - ); - - final embed = EmbedBuilder() - ..color = getRandomColor() - ..title = _enMusicService.trackStuck.title - ..description = _enMusicService.trackStuck.description( - track: track.track.info!.title, - uri: track.track.info!.uri, - requester: track.requester!, - ) - ..thumbnailUrl = - 'https://img.youtube.com/vi/${track.track.info?.identifier}/hqdefault.jpg'; - - await _client.httpEndpoints - .sendMessage(track.channelId!, MessageBuilder.embed(embed)); - } else { - logger.severe( - 'Track stuck: ${player.nowPlaying?.track.info?.title} ' - '(${player.nowPlaying?.track.info?.uri})', - ); - } - } - - Future _trackException(ITrackExceptionEvent event) async { - final player = event.node.players[event.guildId]; - - if (player != null && player.queue.isNotEmpty) { - final track = player.queue[0]; - - logger.severe( - 'MusicService:_trackException - ' - '${queuedTrackToAnalyticsParameters(track)}', - ); - - final embed = EmbedBuilder() - ..color = getRandomColor() - ..title = _enMusicService.trackException.title - ..description = _enMusicService.trackException.description( - track: track.track.info!.title, - uri: track.track.info!.uri, - requester: track.requester!, - ) - ..thumbnailUrl = - 'https://img.youtube.com/vi/${track.track.info?.identifier}/hqdefault.jpg'; - - await _client.httpEndpoints - .sendMessage(track.channelId!, MessageBuilder.embed(embed)); - } - } - - Future voiceStateUpdate(IVoiceStateUpdateEvent event) async { - if (event.state.user.id == _client.appId) return; - if (event.oldState == null) return; - - final eventChannelId = - event.state.channel?.id ?? event.oldState?.channel?.id; - final cachedGuild = event.state.guild; - if (cachedGuild == null || eventChannelId == null) { - logger.severe( - 'VoiceStateUpdate event with null guild or channel: ' - '$cachedGuild, $eventChannelId', - ); - return; - } - - var guild = await cachedGuild.getOrDownload(); - final botMember = await guild.selfMember.getOrDownload(); - - if (botMember.voiceState == null) return; - final currentChannelId = botMember.voiceState?.channel?.id; - - if (currentChannelId == null) { - logger.severe( - 'VoiceStateUpdate event with null bot channel: ' - '$currentChannelId', - ); - return; - } - - if (eventChannelId != currentChannelId) { - // the event is not for the bot's current channel - logger.severe( - 'VoiceStateUpdate event for channel $eventChannelId ' - 'does not match bot channel $currentChannelId', - ); - return; - } - - /// Returns a list of members connected to the same voice channel as the - /// [event] member. Excludes the bot, if present. - bool hasConnectedMembers(IGuild iGuild) { - final voiceStatesInChannel = iGuild.voiceStates.entries.where((element) { - return element.value.channel?.id == currentChannelId && - element.key != botMember.id; - }); - return voiceStatesInChannel.isNotEmpty; - } - - if (!hasConnectedMembers(guild)) { - // Wait 30 seconds before destroying the player - await Future.delayed(const Duration(seconds: 30)); - - // get the guild again in case it was updated - guild = await cachedGuild.download(); - - if (!hasConnectedMembers(guild)) { - try { - cluster.getOrCreatePlayerNode(guild.id).destroy(guild.id); - guild.shard.changeVoiceState(guild.id, null); - - await getIt.get().setNotPlaying(guild.id); - - final channel = await guild.publicUpdatesChannel?.getOrDownload(); - if (channel == null) { - logger.warning( - 'Destroyed player for guild ${guild.id} but the guild ' - 'public updates channel is null', - ); - return; - } - - final commandTranslations = getCommandTranslationsForGuild(guild); - - final embed = EmbedBuilder() - ..color = getRandomColor() - ..title = commandTranslations.leave.left - ..description = commandTranslations.leave.leftDueToInactivity; - - await channel.sendMessage(MessageBuilder.embed(embed)); - } catch (e) { - if (e.toString().contains('LateInitializationError')) return; - logger.severe( - 'Failed to destroy player for guild ${guild.id}: $e', - ); - } - } - } - } -} diff --git a/lib/src/services/services.dart b/lib/src/services/services.dart index 1b1e382..240af21 100644 --- a/lib/src/services/services.dart +++ b/lib/src/services/services.dart @@ -5,6 +5,6 @@ // https://opensource.org/licenses/MIT. export 'bootup.dart'; +export 'bot_start_duration.dart'; export 'db.dart'; -export 'music.dart'; export 'song_recognition.dart'; diff --git a/lib/src/services/song_recognition.dart b/lib/src/services/song_recognition.dart index 9fb98ea..5eac8ca 100644 --- a/lib/src/services/song_recognition.dart +++ b/lib/src/services/song_recognition.dart @@ -9,20 +9,18 @@ import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; +import 'package:injector/injector.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._shazamClient); + SongRecognitionService(); Uuid get uuid => const Uuid(); - ShazamClient get shazamClient => _shazamClient; - - // ShazamClient used to get the sample - final ShazamClient _shazamClient; + ShazamClient get shazamClient => Injector.appInstance.get(); final http.Client httpClient = http.Client(); diff --git a/lib/src/settings.dart b/lib/src/settings.dart index 5204e1f..679f1da 100644 --- a/lib/src/settings.dart +++ b/lib/src/settings.dart @@ -43,7 +43,7 @@ final String prefix = getEnv('RG_PREFIX'); /// The IDs of the users that are allowed to use administrator commands final List adminIds = - getEnv('RG_ADMIN_IDS').split(RegExp(r'\s+')).map(Snowflake.new).toList(); + getEnv('RG_ADMIN_IDS').split(RegExp(r'\s+')).map(Snowflake.parse).toList(); /// The default response for the github command. final String defaultGithubResponse = getEnv( @@ -65,22 +65,22 @@ final bool dev = getEnvBool('RG_DEV'); /// If this instance is in development mode, /// the ID of the guild to register commands to, else `null`. -final devGuildId = dev ? Snowflake(getEnv('RG_DEV_GUILD_ID')) : null; +final devGuildId = dev ? Snowflake.parse(getEnv('RG_DEV_GUILD_ID')) : null; /// The bot's app id. -final clientId = dev ? Snowflake(getEnv('CLIENT_ID')) : null; +final clientId = dev ? Snowflake.parse(getEnv('CLIENT_ID')) : null; /// The address of the lavalink running server to connect to. -String serverAddress = getEnv('LAVALINK_ADDRESS'); +String lavalinkAddress = getEnv('LAVALINK_ADDRESS'); /// The port of the lavalink running server to use to connect. -int serverPort = int.parse(getEnv('LAVALINK_PORT')); +int lavalinkPort = int.parse(getEnv('LAVALINK_PORT')); /// The password used to connect to the lavalink server. -String serverPassword = getEnv('LAVALINK_PASSWORD'); +String lavalinkPassword = getEnv('LAVALINK_PASSWORD'); /// Whether to use or not ssl to establish a connection. -bool useSSL = getEnvBool('LAVALINK_USE_SSL', def: false); +bool lavalinkUseSSL = getEnvBool('LAVALINK_USE_SSL', def: false); /// The api key for the song recognition service, in Rapid Api. /// @@ -92,12 +92,17 @@ String rapidapiShazamSongRecognizerKey = String sentryDsn = getEnv('SENTRY_DSN'); /// The basic intents needed to run Radio Horizon without privileged intents. -const int intents = GatewayIntents.directMessages | +final intents = GatewayIntents.directMessages | GatewayIntents.guilds | - GatewayIntents.guildVoiceState; + GatewayIntents.guildVoiceStates; /// Your MongoDB connection string. /// /// Find yours in https://www.mongodb.com/cloud/atlas. /// You can also use a local MongoDB instance. String mongoDBConnection = getEnv('MONGO_CONNECTION'); + +const botIconUrl = + 'https://cdn.discordapp.com/app-icons/977793621896093736/bed9c0abf5b7f4980024b4ad82a18a15.png?size=256'; + +const githubUrl = 'https://github.com/tomassasovsky/radio-horizon.dart'; diff --git a/lib/src/util.dart b/lib/src/util.dart index b679d36..f2e40cb 100644 --- a/lib/src/util.dart +++ b/lib/src/util.dart @@ -4,9 +4,9 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'dart:io'; import 'dart:math'; import 'package:nyxx/nyxx.dart'; -import 'package:nyxx_interactions/nyxx_interactions.dart'; import 'package:nyxx_lavalink/nyxx_lavalink.dart'; import 'package:radio_horizon/radio_horizon.dart'; @@ -20,31 +20,32 @@ DiscordColor getRandomColor() { ); } -Map queuedTrackToAnalyticsParameters(IQueuedTrack track) { +Map queuedTrackToAnalyticsParameters(Track track) { return { - 'channel_id': track.channelId?.id.toString() ?? 'null', - 'track_uri': track.track.info?.uri ?? 'null', - 'track_title': track.track.info?.title ?? 'null', - 'track_author': track.track.info?.author ?? 'null', - 'track_identifier': track.track.info?.identifier ?? 'null', - 'track_duration': track.track.info?.length.toString() ?? 'null', - 'track_position': track.track.info?.position.toString() ?? 'null', - 'track_is_stream': track.track.info?.stream.toString() ?? 'null', - 'track_is_seekable': track.track.info?.seekable.toString() ?? 'null', - 'track_base64': track.track.track, + 'identifier': track.info.identifier, + 'isSeekable': track.info.isSeekable.toString(), + 'author': track.info.author, + 'length': track.info.length.toString(), + 'isStream': track.info.isStream.toString(), + 'position': track.info.position.toString(), + 'title': track.info.title, + 'uri': track.info.uri.toString(), + 'artworkUrl': track.info.artworkUrl.toString(), + 'isrc': track.info.isrc.toString(), + 'sourceName': track.info.sourceName, }; } Map localizedValues( - String Function(StringsEn translations) getter, + String Function(Translations translations) getter, ) { final locales = AppLocaleUtils.instance.locales; final translations = {}; for (final locale in locales) { final key = Locale.values.firstWhere( - (element) => element.code.startsWith(locale.languageTag), - orElse: () => Locale.englishUs, + (element) => element.identifier.startsWith(locale.languageTag), + orElse: () => Locale.enUs, ); final result = getter(locale.translations); @@ -53,3 +54,5 @@ Map localizedValues( return translations; } + +String getDartPlatform() => Platform.version.split('(').first; diff --git a/lib/src/version.dart b/lib/src/version.dart index 2db1f3c..cb5763e 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '1.2.1'; +const packageVersion = '1.5.0'; diff --git a/portainer.yml b/portainer.yml new file mode 100644 index 0000000..8964781 --- /dev/null +++ b/portainer.yml @@ -0,0 +1,46 @@ +services: + radio_horizon: + image: ghcr.io/tomassasovsky/radio-horizon.dart:latest + container_name: radio_horizon + restart: unless-stopped + expose: + - 8034:8080 + links: + - lavalink + - shazam_api + depends_on: + - lavalink + - shazam_api + # mount a volume for the .env file + volumes: + - /server/docker/radiohorizon/config/.env:/app/.env + + update_lavalink_config: + image: ghcr.io/tomassasovsky/update-lavalink-config:latest + container_name: update_lavalink_config + restart: no + volumes: + - /server/docker/radiohorizon/config/:/output-dir + command: + ["--input", "lavalink.yml", "--output", "/output-dir/output.lavalink.yml"] + + lavalink: + image: ghcr.io/lavalink-devs/lavalink:4 + container_name: lavalink + restart: unless-stopped + expose: + - 2333 + volumes: + - /server/docker/radiohorizon/config/lavalink.yml:/opt/Lavalink/application.yml + depends_on: + update_lavalink_config: + condition: service_completed_successfully + + shazam_api: + image: ghcr.io/tomassasovsky/shazam_api:latest + container_name: shazam_api + restart: unless-stopped + expose: + - 5000 + environment: + FLASK_ENV: production diff --git a/pubspec.yaml b/pubspec.yaml index ed0befb..7542c92 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: radio_horizon description: Radio Horizon discord bot. -version: 1.2.1 +version: 1.5.0 homepage: https://github.com/tomassasovsky/radio_horizon repository: https://github.com/tomassasovsky/radio_horizon documentation: https://github.com/tomassasovsky/radio_horizon @@ -11,39 +11,42 @@ environment: sdk: ">=3.0.0 <4.0.0" 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 + collection: ^1.19.1 + crypto: ^3.0.6 + dotenv: ^4.2.0 + fuzzy: ^0.5.1 + http: ">=0.13.0 <2.0.0" + injector: ^4.0.0 + json2yaml: ^3.0.1 + json_annotation: ^4.9.0 + logging: ^1.3.0 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 + nyxx: ^6.4.3 + nyxx_commands: ^6.0.3 + nyxx_extensions: ^4.2.0 + nyxx_lavalink: 4.0.0-dev.1 radio_browser_api: ^2.0.0+1 retry: ^3.1.2 - sentry: ^7.10.1 - sentry_logging: ^7.10.1 + sentry: ^8.9.0 + sentry_logging: ^8.9.0 shazam_client: path: shazam_client - shelf: ^1.4.1 + shelf: ^1.4.2 shelf_router: ^1.1.4 - slang: ^3.24.0 + slang: ^4.0.0 time_ago_provider: ^4.2.0 - uuid: ^4.0.0 + uuid: ^4.5.1 + yaml_edit: ^2.2.1 dev_dependencies: - build_runner: ^2.4.6 + build_runner: ^2.4.13 build_verify: ^3.1.0 build_version: ^2.1.1 - json_serializable: ^6.7.1 - mocktail: ^1.0.0 - slang_build_runner: ^3.24.0 - test: ^1.24.6 - very_good_analysis: ^5.1.0 + json_serializable: ^6.8.0 + mocktail: ^1.0.4 + slang_build_runner: ^4.0.0 + test: ^1.25.8 + very_good_analysis: ^6.0.0 dependency_overrides: http: ">=0.13.0 <1.1.0" diff --git a/shazam_client/analysis_options.yaml b/shazam_client/analysis_options.yaml index 30f632c..ce4bbaf 100644 --- a/shazam_client/analysis_options.yaml +++ b/shazam_client/analysis_options.yaml @@ -1,4 +1,4 @@ -include: package:very_good_analysis/analysis_options.6.0.0.yaml +include: package:very_good_analysis/analysis_options.yaml linter: rules: diff --git a/test/radio_recognizer_test.dart b/test/radio_recognizer_test.dart index bbb6187..08b546e 100644 --- a/test/radio_recognizer_test.dart +++ b/test/radio_recognizer_test.dart @@ -1,3 +1,4 @@ +import 'package:injector/injector.dart'; import 'package:radio_horizon/radio_horizon.dart'; import 'package:retry/retry.dart'; import 'package:sentry/sentry.dart'; @@ -26,6 +27,10 @@ void main() { ..addIntegration(LoggingIntegration()); }, ); + + Injector.appInstance + ..registerSingleton(ShazamClient.localhost) + ..registerSingleton(SongRecognitionService.new); }); test( @@ -34,8 +39,7 @@ void main() { SongModel? result; await retry( () async { - result = - await SongRecognitionService(ShazamClient.localhost()).identify( + result = await SongRecognitionService().identify( 'https://ais-edge49-nyc04.cdnstream.com/2281_128.mp3', recognitionSampleDuration, ); diff --git a/tool/clean_commands.dart b/tool/clean_commands.dart index 27dc4b0..46ff507 100644 --- a/tool/clean_commands.dart +++ b/tool/clean_commands.dart @@ -1,35 +1,34 @@ -// Copyright (c) 2022, Tomás Sasovsky -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. +// // Copyright (c) 2022, Tomás Sasovsky +// // +// // Use of this source code is governed by an MIT-style +// // license that can be found in the LICENSE file or at +// // https://opensource.org/licenses/MIT. -import 'package:nyxx/nyxx.dart'; -import 'package:nyxx_interactions/nyxx_interactions.dart'; -import 'package:radio_horizon/radio_horizon.dart'; +// import 'package:nyxx/nyxx.dart'; +// import 'package:radio_horizon/radio_horizon.dart'; -Future main(List args) async { - dotEnvFlavour = DotEnvFlavour.development; - dotEnvFlavour.initialize(); +// Future main(List args) async { +// dotEnvFlavour = DotEnvFlavour.development; +// dotEnvFlavour.initialize(); - final bot = NyxxFactory.createNyxxWebsocket( - token, - GatewayIntents.allUnprivileged, - ) - ..registerPlugin(Logging()) // Default logging plugin - ..registerPlugin(CliIntegration()) - ..registerPlugin(IgnoreExceptions()); +// final bot = NyxxFactory.createNyxxWebsocket( +// token, +// GatewayIntents.allUnprivileged, +// ) +// ..registerPlugin(Logging()) // Default logging plugin +// ..registerPlugin(CliIntegration()) +// ..registerPlugin(IgnoreExceptions()); - final interactions = IInteractions.create(WebsocketInteractionBackend(bot)) - ..syncOnReady(); +// // final interactions = IInteractions.create(WebsocketInteractionBackend(bot)) +// // ..syncOnReady(); - await bot.connect(); +// await bot.connect(); - await interactions.interactionsEndpoints.bulkOverrideGuildCommands( - clientId!, - devGuildId!, - [], - ).toList(); +// // await interactions.interactionsEndpoints.bulkOverrideGuildCommands( +// // clientId!, +// // devGuildId!, +// // [], +// // ).toList(); - await bot.dispose(); -} +// await bot.dispose(); +// } diff --git a/update_lavalink_config/Dockerfile b/update_lavalink_config/Dockerfile new file mode 100644 index 0000000..77ec9fa --- /dev/null +++ b/update_lavalink_config/Dockerfile @@ -0,0 +1,20 @@ +# Use the official Dart image as the base +FROM dart:stable AS build + +# Set the working directory +WORKDIR /app +COPY pubspec.* /app/ +RUN dart pub get + +# Copy the rest of the application code +COPY . /app +RUN dart pub get + +# Compile the Dart script to a native executable (optional but recommended) +RUN dart compile exe update_lavalink_config.dart -o update_lavalink_config + +# Set the entrypoint to the compiled executable +ENTRYPOINT ["./update_lavalink_config"] + +# Default arguments (can be overridden) +CMD [] diff --git a/update_lavalink_config/pubspec.yaml b/update_lavalink_config/pubspec.yaml new file mode 100644 index 0000000..32dc80e --- /dev/null +++ b/update_lavalink_config/pubspec.yaml @@ -0,0 +1,11 @@ +name: custom_lavalink +description: A tool that automatically edits the lavalink.yml file to populate the secrets from environment variables. +version: 1.0.0 +publish_to: none + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + args: ^2.6.0 + yaml_edit: ^2.2.1 diff --git a/update_lavalink_config/update_lavalink_config.dart b/update_lavalink_config/update_lavalink_config.dart new file mode 100644 index 0000000..a68bb64 --- /dev/null +++ b/update_lavalink_config/update_lavalink_config.dart @@ -0,0 +1,159 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +// A tool that automatically edits the lavalink.yml file to populate the +// following environment variables: +// +// 1. LAVALINK_PASSWORD +// 2. YOUTUBE_OAUTH_REFRESH_TOKEN +// 3. DEEZER_MASTER_DECRYPTION_KEY +// +// The lavalink.yml file is expected to be in the same directory as this file. +// The environment variables are expected to be in a .env file in the .env +// directory, with the following structure: +// +// .env +// ├── .env.development +// └── .env.production +// +void main(List args) { + final parser = ArgParser() + ..addOption( + 'input', + abbr: 'i', + defaultsTo: 'lavalink.yml', + help: 'Input file path', + ) + ..addOption( + 'output', + abbr: 'o', + help: 'Output file path (defaults to the input file)', + ) + ..addOption( + 'lavalink-password', + help: 'Lavalink password. Will default to the ' + 'LAVALINK_PASSWORD environment variable.', + ) + ..addOption( + 'youtube-oauth-refresh-token', + help: 'YouTube OAuth refresh token. Will default to the ' + 'YOUTUBE_OAUTH_REFRESH_TOKEN environment variable.', + ) + ..addOption( + 'deezer-master-decryption-key', + help: 'Deezer master decryption key. Will default to the ' + 'DEEZER_MASTER_DECRYPTION_KEY environment variable.', + ) + ..addFlag( + 'clear', + negatable: false, + help: 'Clears the existing configuration on the file. ' + 'This is useful to run before commiting anything.', + ) + ..addFlag( + 'help', + abbr: 'h', + negatable: false, + help: 'Display this help message', + ); + + final argResults = parser.parse(args); + + if (argResults['help'] as bool) { + stdout.writeln('Usage: dart script.dart [options]'); + stdout.writeln(parser.usage); + exit(0); + } + + final clear = argResults['clear'] as bool? ?? false; + + // Retrieve the file paths + final inputFilePath = argResults['input'] as String; + final outputFilePath = argResults['output'] as String? ?? inputFilePath; + + // Helper function to get environment variable or command-line argument + String? getEnvOrArg(String argName, String envName) { + if (clear) return null; + return argResults[argName] as String? ?? Platform.environment[envName]; + } + + // Retrieve the environment variables + final lavalinkPassword = + getEnvOrArg('lavalink-password', 'LAVALINK_PASSWORD'); + final youtubeOAuthRefreshToken = + getEnvOrArg('youtube-oauth-refresh-token', 'YOUTUBE_OAUTH_REFRESH_TOKEN'); + final deezerMasterDecryptionKey = getEnvOrArg( + 'deezer-master-decryption-key', + 'DEEZER_MASTER_DECRYPTION_KEY', + ); + + // Ensure required variables are not null + if ((lavalinkPassword == null || + youtubeOAuthRefreshToken == null || + deezerMasterDecryptionKey == null) && + !clear) { + stderr + ..writeln('Error: Missing required environment variables.') + ..writeln( + 'Please provide them via command-line ' + 'arguments or environment variables.', + ); + + exit(1); + } + + final inputFile = File(inputFilePath); + if (!inputFile.existsSync()) { + stderr.writeln('Error: Input file "$inputFilePath" does not exist.'); + exit(1); + } + + final content = inputFile.readAsStringSync(); + + // Create a YamlEditor + final editor = YamlEditor(content); + + // Helper function to ensure a path exists + void ensurePathExists(List path) { + for (var i = 1; i <= path.length; i++) { + final subPath = path.sublist(0, i); + try { + editor.parseAt(subPath); + } catch (e) { + // If the path doesn't exist, create it + editor.update(subPath, {}); + } + } + } + + ensurePathExists(['lavalink', 'server']); + editor.update( + ['lavalink', 'server', 'password'], + lavalinkPassword, + ); + + ensurePathExists(['plugins', 'youtube', 'oauth']); + editor.update( + ['plugins', 'youtube', 'oauth', 'refreshToken'], + youtubeOAuthRefreshToken, + ); + + ensurePathExists(['plugins', 'lavasrc', 'deezer']); + editor.update( + ['plugins', 'lavasrc', 'deezer', 'masterDecryptionKey'], + deezerMasterDecryptionKey, + ); + + final newContent = editor.toString(); + + final outputFile = File(outputFilePath); + outputFile.deleteSync(recursive: true); + outputFile.createSync(recursive: true); + + outputFile.writeAsStringSync(newContent); + + stdout + .writeln('lavalink.yml file updated successfully at "$outputFilePath".'); +}