diff --git a/pom.xml b/pom.xml
index fc91d51..7ee2901 100644
--- a/pom.xml
+++ b/pom.xml
@@ -164,6 +164,11 @@
caffeine
3.1.8
+
+ 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 a497c78..a159d9e 100644
--- a/src/main/java/cc/fascinated/bat/command/Category.java
+++ b/src/main/java/cc/fascinated/bat/command/Category.java
@@ -19,6 +19,7 @@ public enum Category {
MODERATION(Emoji.fromFormatted("U+1F6E0"), "Moderation", false),
UTILITY(Emoji.fromFormatted("U+1F6E0"), "Utility", false),
MUSIC(Emoji.fromFormatted("U+1F3B5"), "Music", false),
+ MOVIES_TV(Emoji.fromFormatted("U+1F3A5"), "Movies & TV", false),
MESSAGES(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..e3a87c4 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -26,6 +26,11 @@ 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: