Skip to content

Commit

Permalink
feat: shazam (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomassasovsky authored Jul 31, 2024
1 parent c57b849 commit e327260
Show file tree
Hide file tree
Showing 59 changed files with 2,292 additions and 270 deletions.
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ coverage/
.flaskenv*
!.env.project
!.env.vault
.history
.history

/shazam_api/lib/
/shazam_api/include/
/shazam_api/__pycache__
/shazam_api/pyvenv.cfg
/shazam_api/pyvenv.cfg
.DS_Store
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ ARG dart_entryfile

WORKDIR /app
COPY pubspec.* /app/
COPY shazam_client /app/
RUN dart pub get

COPY . /app
COPY . /app
RUN dart pub get

Expand Down
1 change: 0 additions & 1 deletion bin/radio_horizon_development.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ Future<void> main() async {
// Initialise our services
MusicService.init(client);
await DatabaseService.init(client);
SongRecognitionService.init(client, DatabaseService.instance);

client.onReady.listen((_) async {
BootUpService.init(client, DatabaseService.instance);
Expand Down
1 change: 0 additions & 1 deletion bin/radio_horizon_production.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ Future<void> main() async {
// Initialise our services
MusicService.init(client);
await DatabaseService.init(client);
SongRecognitionService.init(client, DatabaseService.instance);
BootUpService.init(client, DatabaseService.instance);

// Connect
Expand Down
12 changes: 12 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ services:
- .env/.env.production
links:
- lavalink
- shazam_api
depends_on:
- lavalink
- shazam_api

lavalink:
image: ghcr.io/lavalink-devs/lavalink:3
Expand All @@ -20,3 +22,13 @@ services:
- 2333
volumes:
- ./lavalink.yml:/opt/Lavalink/application.yml

shazam_api:
build:
context: ./shazam_api
expose:
- 5000
volumes:
- ./shazam_api:/app
environment:
FLASK_ENV: production
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ services:
- .env/.env.development
links:
- lavalink
- shazam_api
depends_on:
- lavalink
- shazam_api

lavalink:
image: ghcr.io/lavalink-devs/lavalink:3
Expand All @@ -23,3 +25,13 @@ services:
- 2333
volumes:
- ./lavalink.yml:/opt/Lavalink/application.yml

shazam_api:
build:
context: ./shazam_api
expose:
- 5000
volumes:
- ./shazam_api:/app
environment:
FLASK_ENV: development
2 changes: 1 addition & 1 deletion lib/src/commands/music.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ ChatCommand:music-play: {
);
}

await SongRecognitionService.instance
await DatabaseService.instance
.deleteRadioFromList(context.guild!.id);
}),
localizedDescriptions: localizedValues(
Expand Down
81 changes: 19 additions & 62 deletions lib/src/commands/radio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import 'package:logging/logging.dart';
import 'package:nyxx/nyxx.dart';
import 'package:nyxx_commands/nyxx_commands.dart';
import 'package:nyxx_interactions/nyxx_interactions.dart';
import 'package:nyxx_pagination/nyxx_pagination.dart';
import 'package:radio_browser_api/radio_browser_api.dart';
import 'package:radio_horizon/radio_horizon.dart';
import 'package:radio_horizon/src/checks.dart';
import 'package:radio_horizon/src/models/song_recognition/current_station_info.dart';
import 'package:retry/retry.dart';
import 'package:shazam_client/shazam_client.dart';

final _enRadioCommand = AppLocale.en.translations.commands.radio;
final _enPlayCommand = _enRadioCommand.children.play;
Expand Down Expand Up @@ -124,7 +124,8 @@ ChatCommand:radio-play: {
channelId: context.channel.id,
).startPlaying();

await SongRecognitionService.instance.setCurrentRadio(
final databaseService = DatabaseService.instance;
await databaseService.setCurrentRadio(
context.guild!.id,
context.member!.voiceState!.channel!.id,
context.channel.id,
Expand Down Expand Up @@ -158,39 +159,18 @@ ChatCommand:radio-play: {
final translations = getCommandTranslations(context);
final commandTranslations = translations.radio.children.recognize;
CurrentStationInfo? stationInfo;
MusicLinksResponse? linksResponse;

try {
final recognitionService = SongRecognitionService.instance;
final databaseService = DatabaseService.instance;
final guildId = context.guild!.id;

final stopwatch = Stopwatch()..start();
var recognitionSampleDuration = 10;

final guildRadio = await recognitionService.currentRadio(guildId);
final guildRadio = await databaseService.currentRadio(guildId);

try {
final info = await retry(
() async => recognitionService.getCurrentStationInfo(guildRadio),
);
if (!info.hasTitle) {
throw Exception('No title');
}
final node = MusicService.instance.cluster
.getOrCreatePlayerNode(context.guild!.id);
final tracks = await node.autoSearch(info.title!);
stationInfo = info.copyWith(
image:
'https://img.youtube.com/vi/${tracks.tracks.first.info?.identifier}/hqdefault.jpg',
);
} catch (exception, stacktrace) {
_logger.severe(
'Failed to get current station info',
exception,
stacktrace,
);

ShazamResult? result;
SongModel? result;
await retry(
() async {
result = await recognitionService.identify(
Expand Down Expand Up @@ -219,21 +199,18 @@ ChatCommand:radio-play: {

stationInfo =
CurrentStationInfo.fromShazamResult(result!, guildRadio);
}

try {
linksResponse = await SongRecognitionService.instance
.getMusicLinks(stationInfo.title!);
} catch (exception, stacktrace) {
_logger.severe(
'Failed to get music links for ${stationInfo.title}',
exception,
stacktrace,
} catch (e) {
await context.respond(
MessageBuilder.embed(
EmbedBuilder()
..color = DiscordColor.red
..title = commandTranslations.errors.noResults,
),
);
return null;
}

final color = getRandomColor();
stopwatch.stop();

final embed = EmbedBuilder()
..color = color
Expand All @@ -245,7 +222,8 @@ ChatCommand:radio-play: {
name: commandTranslations.radioStationField,
content: stationInfo.name,
)
..thumbnailUrl = stationInfo.image;
..thumbnailUrl = stationInfo.image
..url = stationInfo.url;

final genre = stationInfo.genre;
if (genre != null) {
Expand All @@ -255,28 +233,7 @@ ChatCommand:radio-play: {
);
}

embed.addField(
name: commandTranslations.computationalTimeField,
content: '${stopwatch.elapsedMilliseconds}ms',
);

final lyricsPages = stationInfo.lyricsPages(color: color);
if (lyricsPages == null || lyricsPages.isEmpty) {
final builder = ComponentMessageBuilder()..embeds = [embed];
linksResponse?.componentRows.forEach(builder.addComponentRow);
return await context.respond(builder);
}

final paginator = EmbedComponentPagination(
context.commands.interactions!,
[embed, ...lyricsPages],
user: context.user,
);

final messageBuilder = paginator.initMessageBuilder();
linksResponse?.componentRows.forEach(messageBuilder.addComponentRow);

await context.respond(messageBuilder);
await context.respond(MessageBuilder.embed(embed));
} catch (e, stacktrace) {
_logger.severe(
'Failed to recognize radio',
Expand Down Expand Up @@ -316,8 +273,8 @@ ChatCommand:radio-play: {

late GuildRadio? guildRadio;
try {
guildRadio = await SongRecognitionService.instance
.currentRadio(context.guild!.id);
guildRadio =
await DatabaseService.instance.currentRadio(context.guild!.id);
} on RadioNotPlayingException {
await context.respond(
MessageBuilder.embed(
Expand Down
78 changes: 15 additions & 63 deletions lib/src/models/song_recognition/current_station_info.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:nyxx/nyxx.dart';
import 'package:radio_horizon/radio_horizon.dart';
import 'package:shazam_client/shazam_client.dart';

part 'current_station_info.g.dart';

Expand All @@ -14,34 +14,26 @@ class CurrentStationInfo {
this.title,
this.image,
this.url,
}) : _lyrics = null;

const CurrentStationInfo._lyrics({
this.description,
this.genre,
this.name,
this.title,
this.image,
this.url,
List<String>? lyrics,
}) : _lyrics = lyrics,
contentType = null;
});

factory CurrentStationInfo.fromJson(Map<String, dynamic> json) =>
_$CurrentStationInfoFromJson(json);

factory CurrentStationInfo.fromShazamResult(
ShazamResult result,
GuildRadio radio,
) =>
CurrentStationInfo._lyrics(
name: result.headline,
title: radio.station.name,
SongModel result, [
GuildRadio? guildRadio,
]) =>
CurrentStationInfo(
title: '${result.title} - ${result.subtitle}',
description: result.subtitle,
url: radio.station.urlResolved ?? radio.station.url,
image: result.share?.image ?? radio.station.favicon,
url: Uri.https(
'youtube.com',
'/results',
{'search_query': result.title},
).toString(),
image: result.images?.coverart,
genre: result.genres?.primary,
lyrics: result.lyrics,
name: guildRadio?.station.name,
);

CurrentStationInfo copyWith({
Expand All @@ -53,8 +45,7 @@ class CurrentStationInfo {
String? url,
List<String>? lyrics,
}) =>
CurrentStationInfo._lyrics(
lyrics: lyrics ?? _lyrics,
CurrentStationInfo(
description: description ?? this.description,
genre: genre ?? this.genre,
name: name ?? this.name,
Expand Down Expand Up @@ -83,46 +74,7 @@ class CurrentStationInfo {
final String? url;

final String? image;
final List<String>? _lyrics;

bool get hasName => name != null && name!.isNotEmpty;
bool get hasTitle => title != null && title!.isNotEmpty;

List<List<String>>? paragraphedLyrics(int paragraphsPerPage) {
if (_lyrics == null || _lyrics!.isEmpty) return null;
final lyrics = _lyrics ?? [];
final paragraphs = lyrics.join('\n').split('\n\n');

final m = (paragraphs.length / paragraphsPerPage).round();
final lists = List.generate(
3,
(i) => paragraphs.sublist(
m * i,
(i + 1) * m <= paragraphs.length ? (i + 1) * m : null,
),
);

return lists;
}

List<EmbedBuilder>? lyricsPages({
required DiscordColor color,
}) {
final lyricsPages = <EmbedBuilder>[];
final llyrics = paragraphedLyrics(3);

if (llyrics == null) return null;

// add 3 paragraphs per page
for (var i = 0; i < llyrics.length; i++) {
final embed = EmbedBuilder()
..color = color
..title = title
..description = llyrics[i].join('\n\n');

lyricsPages.add(embed);
}

return lyricsPages;
}
}
Loading

0 comments on commit e327260

Please sign in to comment.