diff --git a/pom.xml b/pom.xml index 591badd..184c9ef 100644 --- a/pom.xml +++ b/pom.xml @@ -159,6 +159,11 @@ spotify-web-api-java 8.4.0 + + uk.co.conoregan + themoviedbapi + 2.1.1 + diff --git a/src/main/java/cc/fascinated/bat/command/Category.java b/src/main/java/cc/fascinated/bat/command/Category.java index e156033..b22fc6e 100644 --- a/src/main/java/cc/fascinated/bat/command/Category.java +++ b/src/main/java/cc/fascinated/bat/command/Category.java @@ -18,6 +18,7 @@ public enum Category { SERVER(Emoji.fromFormatted("U+1F5A5"), "Server", false), UTILITY(Emoji.fromFormatted("U+1F6E0"), "Utility", false), MUSIC(Emoji.fromFormatted("U+1F3B5"), "Music", false), + MOVIES_TV(Emoji.fromFormatted("U+1F37F"), "Movies & TV", false), SNIPE(Emoji.fromFormatted("U+1F4A3"), "Snipe", false), LOGS(Emoji.fromFormatted("U+1F4D1"), "Logs", false), BEAT_SABER(Emoji.fromFormatted("U+1FA84"), "Beat Saber", false), diff --git a/src/main/java/cc/fascinated/bat/features/spotify/SpotifyFeature.java b/src/main/java/cc/fascinated/bat/features/spotify/SpotifyFeature.java index 2351415..f339882 100644 --- a/src/main/java/cc/fascinated/bat/features/spotify/SpotifyFeature.java +++ b/src/main/java/cc/fascinated/bat/features/spotify/SpotifyFeature.java @@ -29,7 +29,7 @@ import se.michaelthelin.spotify.model_objects.specification.Track; public class SpotifyFeature extends Feature { @Autowired public SpotifyFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) { - super("Spotify", true,Category.MUSIC); + super("Spotify", true, Category.MUSIC); super.registerCommand(commandService, context.getBean(SpotifyCommand.class)); } diff --git a/src/main/java/cc/fascinated/bat/features/tmdb/TMDBFeature.java b/src/main/java/cc/fascinated/bat/features/tmdb/TMDBFeature.java new file mode 100644 index 0000000..3e26136 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/tmdb/TMDBFeature.java @@ -0,0 +1,102 @@ +package cc.fascinated.bat.features.tmdb; + +import cc.fascinated.bat.command.Category; +import cc.fascinated.bat.common.EmbedUtils; +import cc.fascinated.bat.common.NumberFormatter; +import cc.fascinated.bat.features.Feature; +import cc.fascinated.bat.features.tmdb.command.TMDBCommand; +import cc.fascinated.bat.service.CommandService; +import cc.fascinated.bat.service.TMDBService; +import info.movito.themoviedbapi.model.core.Movie; +import info.movito.themoviedbapi.model.core.TvSeries; +import lombok.NonNull; +import net.dv8tion.jda.api.EmbedBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @author Nick (okNick) + */ +@Component +public class TMDBFeature extends Feature { + @Autowired + public TMDBFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) { + super("TMDB", true, Category.MOVIES_TV); + + super.registerCommand(commandService, context.getBean(TMDBCommand.class)); + } + + /** + * Create an embed for a movie page. + * + * @param tmdbService The TMDB service. + * @param query The query to search for. + * @param language The language to search in. + * @param primaryReleaseYear The primary release year to filter by. + * @param region The region to search in. + * @param year The year to filter by. + * @param movie The movie index. + * @param adult Whether to include adult content. + * @return The movie page embed. + */ + public static EmbedBuilder pageMovie(@NonNull TMDBService tmdbService, @NonNull String query, String language, String primaryReleaseYear, String region, String year, int movie, boolean adult) { + List movieList = tmdbService.lookupMovies(query, adult, language, primaryReleaseYear, region, year); + + if (movieList.isEmpty()) { + return EmbedUtils.errorEmbed() + .setDescription("No movieList found with the provided query + options!"); + } + + // Adjust movie index to stay within bounds + if (movie >= movieList.size()) { + movie = movieList.size() - 1; // Set to the last movie if index exceeds list size + } + + return EmbedUtils.genericEmbed() + .setAuthor(movieList.get(movie).getTitle(), "https://www.themoviedb.org/movie/%s".formatted(movieList.get(movie).getId())) + .setThumbnail("https://media.themoviedb.org/t/p/w220_and_h330_face%s".formatted(movieList.get(movie).getPosterPath())) + .setDescription(movieList.get(movie).getOverview()) + .addField("Release Date", movieList.get(movie).getReleaseDate(), true) + .addField("Rating", NumberFormatter.format(movieList.get(movie).getVoteAverage()) + "/10", true) + .addField("Language", movieList.get(movie).getOriginalLanguage(), true) + .setFooter("Page %s of %s".formatted(movie + 1, movieList.size())); + } + + /** + * Create an embed for a series page. + * + * @param tmdbService The TMDB service. + * @param query The query to search for. + * @param language The language to search in. + * @param firstAirDateYear The first air date year to filter by. + * @param year The year to filter by. + * @param series The series index. + * @param adult Whether to include adult content. + * @return The series page embed. + */ + public static EmbedBuilder pageSeries(@NonNull TMDBService tmdbService, @NonNull String query, String language, int firstAirDateYear, int year, int series, boolean adult) { + List seriesList = tmdbService.lookupSeries(query, adult, language, firstAirDateYear, year); + + if (seriesList.isEmpty()) { + return EmbedUtils.errorEmbed() + .setDescription("No series found with the provided query + options!"); + } + + // Adjust series index to stay within bounds + if (series >= seriesList.size()) { + series = seriesList.size() - 1; // Set to the last series if index exceeds list size + } + + return EmbedUtils.genericEmbed() + .setAuthor(seriesList.get(series).getName(), "https://www.themoviedb.org/tv/%s".formatted(seriesList.get(series).getId())) + .setThumbnail("https://media.themoviedb.org/t/p/w220_and_h330_face%s".formatted(seriesList.get(series).getPosterPath())) + .setDescription(seriesList.get(series).getOverview()) + .addField("First Air Date", seriesList.get(series).getFirstAirDate(), true) + .addField("Rating", NumberFormatter.format(seriesList.get(series).getVoteAverage()) + "/10", true) + .addField("Language", seriesList.get(series).getOriginalLanguage(), true) + .setFooter("Page %s of %s".formatted(series + 1, seriesList.size())); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/features/tmdb/command/MovieSubCommand.java b/src/main/java/cc/fascinated/bat/features/tmdb/command/MovieSubCommand.java new file mode 100644 index 0000000..965c4ae --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/tmdb/command/MovieSubCommand.java @@ -0,0 +1,134 @@ +package cc.fascinated.bat.features.tmdb.command; + +import cc.fascinated.bat.command.BatSubCommand; +import cc.fascinated.bat.command.CommandInfo; +import cc.fascinated.bat.common.EmbedUtils; +import cc.fascinated.bat.event.EventListener; +import cc.fascinated.bat.features.tmdb.TMDBFeature; +import cc.fascinated.bat.model.BatGuild; +import cc.fascinated.bat.model.BatUser; +import cc.fascinated.bat.service.TMDBService; +import lombok.NonNull; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +@CommandInfo(name = "movie", description = "Get information about a movie") +public class MovieSubCommand extends BatSubCommand implements EventListener { + + private final TMDBService tmdbService; + private final Map> userCommands; // Map to store user commands and their parameters + + @Autowired + public MovieSubCommand(@NonNull TMDBService tmdbService) { + this.tmdbService = tmdbService; + this.userCommands = new HashMap<>(); + + super.addOption(OptionType.STRING, "title", "The title of the movie", true); + super.addOption(OptionType.STRING, "language", "A locale code (en-US) to lookup movies in a specific language", false); + super.addOption(OptionType.STRING, "primary_release_year", "Filter the results so that only the primary release dates have this value", false); + super.addOption(OptionType.STRING, "region", "An ISO 3166-1 code (US) to lookup movies from a specific region", false); + super.addOption(OptionType.STRING, "year", "Filter the results release dates to matches that include this value", false); + } + + @Override + public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) { + OptionMapping titleOption = event.getOption("title"); + if (titleOption == null) { + event.replyEmbeds(EmbedUtils.errorEmbed() + .setDescription("You must provide a title to search for!") + .build()) + .queue(); + return; + } + + // Determine if the channel is NSFW. If so, allow adult content + boolean adult = false; + if (event.getChannel() instanceof TextChannel textChannel) { + adult = textChannel.isNSFW(); + } + + OptionMapping languageOption = event.getOption("language"); + OptionMapping primaryReleaseYearOption = event.getOption("primary_release_year"); + OptionMapping regionOption = event.getOption("region"); + OptionMapping yearOption = event.getOption("year"); + + // Store user command and parameters for later use + Map params = new HashMap<>(); + params.put("title", titleOption.getAsString()); + if (languageOption != null) params.put("language", languageOption.getAsString()); + if (primaryReleaseYearOption != null) params.put("primary_release_year", primaryReleaseYearOption.getAsString()); + if (regionOption != null) params.put("region", regionOption.getAsString()); + if (yearOption != null) params.put("year", yearOption.getAsString()); + params.put("adult", String.valueOf(adult)); + + userCommands.put(user.getId(), params); + + event.replyEmbeds(TMDBFeature.pageMovie( + tmdbService, + titleOption.getAsString(), + (languageOption != null ? languageOption.getAsString() : null), + (primaryReleaseYearOption != null ? primaryReleaseYearOption.getAsString() : null), + (regionOption != null ? regionOption.getAsString() : null), + (yearOption != null ? yearOption.getAsString() : null), + 0, // Initial page number + adult + ).build() + ).addActionRow( + Button.primary("backMovie", "Back").withEmoji(Emoji.fromFormatted("⬅️")), + Button.primary("nextMovie", "Next").withEmoji(Emoji.fromFormatted("➡️")) + ).queue(); + } + + @Override + public void onButtonInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ButtonInteractionEvent event) { + Map params = userCommands.get(user.getId()); + if (params == null) { + return; + } + + int currentPage = Integer.parseInt(params.getOrDefault("page", "0")); + boolean adult = Boolean.parseBoolean(params.get("adult")); + + // Retrieve stored parameters + String title = params.get("title"); + String language = params.get("language"); + String primaryReleaseYear = params.get("primary_release_year"); + String region = params.get("region"); + String year = params.get("year"); + + if (event.getComponentId().equals("backMovie")) { + currentPage--; + if (currentPage < 0) { + currentPage = 0; // Ensure currentPage doesn't go negative + } + } else if (event.getComponentId().equals("nextMovie")) { + currentPage++; + } + + params.put("page", String.valueOf(currentPage)); + + event.editMessageEmbeds(TMDBFeature.pageMovie( + tmdbService, + title, + language, + primaryReleaseYear, + region, + year, + currentPage, + adult + ).build()).queue(); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/features/tmdb/command/SeriesSubCommand.java b/src/main/java/cc/fascinated/bat/features/tmdb/command/SeriesSubCommand.java new file mode 100644 index 0000000..f7f4316 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/tmdb/command/SeriesSubCommand.java @@ -0,0 +1,128 @@ +package cc.fascinated.bat.features.tmdb.command; + +import cc.fascinated.bat.command.BatSubCommand; +import cc.fascinated.bat.command.CommandInfo; +import cc.fascinated.bat.common.EmbedUtils; +import cc.fascinated.bat.event.EventListener; +import cc.fascinated.bat.features.tmdb.TMDBFeature; +import cc.fascinated.bat.model.BatGuild; +import cc.fascinated.bat.model.BatUser; +import cc.fascinated.bat.service.TMDBService; +import lombok.NonNull; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +@CommandInfo(name = "series", description = "Get information about a series") +public class SeriesSubCommand extends BatSubCommand implements EventListener { + + private final TMDBService tmdbService; + private final Map> userCommands; // Map to store user commands and their parameters + + @Autowired + public SeriesSubCommand(@NonNull TMDBService tmdbService) { + this.tmdbService = tmdbService; + this.userCommands = new HashMap<>(); + + super.addOption(OptionType.STRING, "title", "The title of the series", true); + super.addOption(OptionType.STRING, "language", "A locale code (en-US) to lookup movies in a specific language", false); + super.addOption(OptionType.INTEGER, "first_air_year", "Filter the results so that only the first air year has this value", false); + super.addOption(OptionType.INTEGER, "year", "Filter the results release dates to matches that include this value", false); + } + + @Override + public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) { + OptionMapping titleOption = event.getOption("title"); + if (titleOption == null) { + event.replyEmbeds(EmbedUtils.errorEmbed() + .setDescription("You must provide a title to search for!") + .build()) + .queue(); + return; + } + + // Determine if the channel is NSFW. If so, allow adult content + boolean adult = false; + if (event.getChannel() instanceof TextChannel textChannel) { + adult = textChannel.isNSFW(); + } + + OptionMapping languageOption = event.getOption("language"); + OptionMapping firstAirYearOption = event.getOption("first_air_year"); + OptionMapping yearOption = event.getOption("year"); + + // Store user command and parameters for later use + Map params = new HashMap<>(); + params.put("title", titleOption.getAsString()); + if (languageOption != null) params.put("language", languageOption.getAsString()); + if (firstAirYearOption != null) params.put("first_air_year", String.valueOf(firstAirYearOption.getAsInt())); + if (yearOption != null) params.put("year", String.valueOf(yearOption.getAsInt())); + params.put("adult", String.valueOf(adult)); + + userCommands.put(user.getId(), params); + + event.replyEmbeds(TMDBFeature.pageSeries( + tmdbService, + titleOption.getAsString(), + (languageOption != null ? languageOption.getAsString() : null), + (firstAirYearOption != null ? firstAirYearOption.getAsInt() : -1), + (yearOption != null ? yearOption.getAsInt() : -1), + 0, // Initial page number + adult + ).build() + ).addActionRow( + Button.primary("backSeries", "Back").withEmoji(Emoji.fromFormatted("⬅️")), + Button.primary("nextSeries", "Next").withEmoji(Emoji.fromFormatted("➡️")) + ).queue(); + } + + @Override + public void onButtonInteraction(BatGuild guild, @NonNull BatUser user, @NonNull ButtonInteractionEvent event) { + Map params = userCommands.get(user.getId()); + if (params == null) { + return; + } + + int currentPage = Integer.parseInt(params.getOrDefault("page", "0")); + boolean adult = Boolean.parseBoolean(params.get("adult")); + + // Retrieve stored parameters + String title = params.get("title"); + String language = params.get("language"); + int firstAirYear = (params.get("first_air_year") != null ? Integer.parseInt(params.get("first_air_year")) : -1); + int year = (params.get("year") != null ? Integer.parseInt(params.get("year")) : -1); + + if (event.getComponentId().equals("backSeries")) { + currentPage--; + if (currentPage < 0) { + currentPage = 0; // Ensure currentPage doesn't go negative + } + } else if (event.getComponentId().equals("nextSeries")) { + currentPage++; + } + + params.put("page", String.valueOf(currentPage)); + + event.editMessageEmbeds(TMDBFeature.pageSeries( + tmdbService, + title, + language, + firstAirYear, + year, + currentPage, + adult + ).build()).queue(); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/features/tmdb/command/TMDBCommand.java b/src/main/java/cc/fascinated/bat/features/tmdb/command/TMDBCommand.java new file mode 100644 index 0000000..45f2e0c --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/tmdb/command/TMDBCommand.java @@ -0,0 +1,21 @@ +package cc.fascinated.bat.features.tmdb.command; + +import cc.fascinated.bat.command.BatCommand; +import cc.fascinated.bat.command.CommandInfo; +import lombok.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @author Nick (okNick) + */ +@Component +@CommandInfo(name = "tmdb", description = "Get information about movies and TV shows", guildOnly = false) +public class TMDBCommand extends BatCommand { + @Autowired + public TMDBCommand(@NonNull ApplicationContext context) { + super.addSubCommand(context.getBean(MovieSubCommand.class)); + super.addSubCommand(context.getBean(SeriesSubCommand.class)); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/bat/service/TMDBService.java b/src/main/java/cc/fascinated/bat/service/TMDBService.java new file mode 100644 index 0000000..5c1176b --- /dev/null +++ b/src/main/java/cc/fascinated/bat/service/TMDBService.java @@ -0,0 +1,77 @@ +package cc.fascinated.bat.service; + +import info.movito.themoviedbapi.TmdbApi; +import info.movito.themoviedbapi.model.core.Movie; +import info.movito.themoviedbapi.model.core.MovieResultsPage; +import info.movito.themoviedbapi.model.core.TvSeries; +import info.movito.themoviedbapi.model.core.TvSeriesResultsPage; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author Nick (okNick) + */ +@Service +@Getter +@Log4j2(topic = "TMDB Service") +public class TMDBService { + /** + * The API key. + */ + private final String apiKey; + + /** + * The TMDB API instance. + */ + private final TmdbApi tmdbApi; + + public TMDBService(@Value("${tmdb.api-key}") String apiKey) { + this.apiKey = apiKey; + + this.tmdbApi = new TmdbApi(apiKey); + } + + /** + * Lookup movies based on the provided query and options. + * + * @param query The query to search for + * @param includeAdult Whether to include adult content + * @param language The language to search in + * @param primaryReleaseYear The primary release year to filter by + * @param region The region to search in + * @param year The year to filter by + * @return The list of movies found with the provided query and options + */ + @SneakyThrows + public List lookupMovies(String query, boolean includeAdult, String language, String primaryReleaseYear, String region, String year) { + MovieResultsPage movies = tmdbApi.getSearch().searchMovie(query, includeAdult, language, primaryReleaseYear, 1, region, year); + if (movies.getTotalResults() == 0) { + return null; + } + return movies.getResults(); + } + + /** + * Lookup series based on the provided query and options. + * + * @param query The query to search for + * @param includeAdult Whether to include adult content + * @param language The language to search in + * @param firstAirDateYear The first air date year to filter by + * @param year The year to filter by + * @return The list of series found with the provided query and options + */ + @SneakyThrows + public List lookupSeries(String query, boolean includeAdult, String language, int firstAirDateYear, int year) { + TvSeriesResultsPage series = tmdbApi.getSearch().searchTv(query, firstAirDateYear, includeAdult, language, 1, year); + if (series.getTotalResults() == 0) { + return null; + } + return series.getResults(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4ef3db1..b5acdd8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,6 +26,12 @@ spotify: client-id: "spotify-client-id" client-secret: "spotify-client-secret" +# TMDB Configuration +tmdb: + # API Read Access Token + api-key: "api-read-access-token" + + # Spring Configuration spring: data: