diff --git a/build.gradle b/build.gradle index 7af60d6..1a86264 100644 --- a/build.gradle +++ b/build.gradle @@ -64,6 +64,10 @@ dependencies { compile 'mysql:mysql-connector-java' compile 'org.reflections:reflections:0.9.11' compile 'org.apache.commons:commons-lang3:3.7' + compile 'com.google.api-client:google-api-client:1.23.0' + compile 'com.google.apis:google-api-services-youtube:v3-rev192-1.23.0' + compile 'com.google.oauth-client:google-oauth-client-java6:1.23.0' + compile 'com.google.oauth-client:google-oauth-client-jetty:1.23.0' testCompile('org.springframework.boot:spring-boot-starter-test') testCompile('com.jayway.jsonpath:json-path') diff --git a/src/main/java/net/Broken/Commands/Music.java b/src/main/java/net/Broken/Commands/Music.java index 4d5a09b..5df2cba 100644 --- a/src/main/java/net/Broken/Commands/Music.java +++ b/src/main/java/net/Broken/Commands/Music.java @@ -23,7 +23,7 @@ public class Music implements Commande { public AudioM audio; Logger logger = LogManager.getLogger(); public Music() { - audio = new AudioM(MainBot.jda.getGuilds().get(0)); + audio = AudioM.getInstance(MainBot.jda.getGuilds().get(0)); } @Override diff --git a/src/main/java/net/Broken/Commands/ytTest.java b/src/main/java/net/Broken/Commands/ytTest.java new file mode 100644 index 0000000..a75c9ba --- /dev/null +++ b/src/main/java/net/Broken/Commands/ytTest.java @@ -0,0 +1,42 @@ +package net.Broken.Commands; + +import net.Broken.Commande; +import net.Broken.audio.AudioM; +import net.Broken.audio.NotConnectedException; +import net.Broken.audio.NullMusicManager; +import net.Broken.audio.Youtube.YoutubeTools; +import net.dv8tion.jda.core.events.message.MessageReceivedEvent; + +import java.io.IOException; + +public class ytTest implements Commande { + @Override + public void action(String[] args, MessageReceivedEvent event) { + YoutubeTools yt = YoutubeTools.getInstance(null); + +// try { +//// event.getTextChannel().sendMessage(yt.getRelatedVideo(args[0])).queue(); +//// AudioM.getInstance(null).getGuildMusicManager().scheduler.autoPlay(); +// +// } catch (NotConnectedException e) { +// e.printStackTrace(); +// } catch (NullMusicManager nullMusicManager) { +// nullMusicManager.printStackTrace(); +// } + } + + @Override + public boolean isPrivateUsable() { + return false; + } + + @Override + public boolean isAdminCmd() { + return false; + } + + @Override + public boolean isNSFW() { + return false; + } +} diff --git a/src/main/java/net/Broken/Init.java b/src/main/java/net/Broken/Init.java index aaeffa8..08e20a2 100644 --- a/src/main/java/net/Broken/Init.java +++ b/src/main/java/net/Broken/Init.java @@ -4,6 +4,7 @@ import net.Broken.Tools.Command.CommandLoader; import net.Broken.Tools.DayListener.DayListener; import net.Broken.Tools.DayListener.Listeners.DailyMadame; import net.Broken.Tools.DayListener.Listeners.ResetSpam; +import net.Broken.audio.Youtube.YoutubeTools; import net.dv8tion.jda.core.AccountType; import net.dv8tion.jda.core.JDA; import net.dv8tion.jda.core.JDABuilder; @@ -16,6 +17,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.security.auth.login.LoginException; +import java.io.IOException; import java.util.List; @@ -56,6 +58,7 @@ public class Init { *************************************/ jda.getPresence().setGame(Game.of("Statut: Loading...")); jda.getTextChannels().forEach(textChannel -> textChannel.sendTyping().queue()); + YoutubeTools.getInstance(jda.getGuilds().get(0)).getYouTubeService(); CommandLoader.load(); @@ -94,6 +97,7 @@ public class Init { dayListener.addListener(new DailyMadame()); dayListener.start(); + logger.debug("-----------------FIN INITIALISATION-----------------"); jda.getPresence().setGame(Game.of("Statut: Ok!")); @@ -102,6 +106,8 @@ public class Init { catch (LoginException | InterruptedException | RateLimitedException e) { logger.catching(e); + } catch (IOException e) { + e.printStackTrace(); } } diff --git a/src/main/java/net/Broken/RestApi/Commands/Connect.java b/src/main/java/net/Broken/RestApi/Commands/Connect.java index 42f6975..d581d78 100644 --- a/src/main/java/net/Broken/RestApi/Commands/Connect.java +++ b/src/main/java/net/Broken/RestApi/Commands/Connect.java @@ -16,7 +16,7 @@ import org.springframework.http.ResponseEntity; public class Connect implements CommandInterface{ @Override public ResponseEntity action(Music musicCommande, CommandPostData data, User user) { - AudioM audioM = musicCommande.getAudioManager(); + AudioM audioM = AudioM.getInstance(null); if(data.chanelId == null) return new ResponseEntity<>(new CommandResponseData(data.command,"Missing chanelId"),HttpStatus.BAD_REQUEST); VoiceChannel voiceChannel = null; diff --git a/src/main/java/net/Broken/audio/AudioM.java b/src/main/java/net/Broken/audio/AudioM.java index 14c4b9f..1b85475 100644 --- a/src/main/java/net/Broken/audio/AudioM.java +++ b/src/main/java/net/Broken/audio/AudioM.java @@ -52,10 +52,19 @@ public class AudioM { private Guild guild; private Logger logger = LogManager.getLogger(); + private static AudioM INSTANCE; + + public static AudioM getInstance(Guild guild){ + if(INSTANCE == null){ + INSTANCE = new AudioM(guild); + } + + return INSTANCE; + } - public AudioM(Guild guild) { + private AudioM(Guild guild) { this.playerManager = new DefaultAudioPlayerManager(); AudioSourceManagers.registerRemoteSources(playerManager); AudioSourceManagers.registerLocalSource(playerManager); @@ -80,11 +89,7 @@ public class AudioM { logger.info("Single Track detected!"); UserAudioTrack uat = new UserAudioTrack(event.getAuthor(), track); Message message = event.getTextChannel().sendMessage(EmbedMessageUtils.getMusicOk("Ajout de "+track.getInfo().title+" à la file d'attente!")).complete(); - List messages = new ArrayList(){{ - add(message); - add(event.getMessage()); - }}; - new MessageTimeOut(messages, MainBot.messageTimeOut).start(); + new MessageTimeOut(MainBot.messageTimeOut, message, event.getMessage()).start(); play(guild, voiceChannel, musicManager, uat, onHead); } @@ -95,11 +100,7 @@ public class AudioM { AudioTrack firstTrack = playlist.getSelectedTrack(); Message message = event.getTextChannel().sendMessage(EmbedMessageUtils.getMusicOk("Ajout de "+firstTrack.getInfo().title+" et les 30 premiers titres à la file d'attente!")).complete(); - List messages = new ArrayList(){{ - add(message); - add(event.getMessage()); - }}; - new MessageTimeOut(messages, MainBot.messageTimeOut).start(); + new MessageTimeOut(MainBot.messageTimeOut, message, event.getMessage()).start(); playListLoader(playlist, playlistLimit ,event.getAuthor() , onHead); @@ -132,6 +133,37 @@ public class AudioM { }); } + public void loadAndPlayAuto(String trackUrl){ + playerManager.loadItemOrdered(musicManager, trackUrl, new AudioLoadResultHandler(){ + @Override + public void trackLoaded(AudioTrack track) { + logger.info("Auto add " + track.getInfo().title +" to playlist."); + UserAudioTrack userAudioTrack = new UserAudioTrack(MainBot.jda.getSelfUser(),track); + play(guild, playedChanel, musicManager, userAudioTrack, true); + } + + @Override + public void playlistLoaded(AudioPlaylist playlist) { + AudioTrack track = playlist.getTracks().get(0); + logger.info("Auto add " + track.getInfo().title +" to playlist."); + UserAudioTrack userAudioTrack = new UserAudioTrack(MainBot.jda.getSelfUser(),track); + play(guild, playedChanel, musicManager, userAudioTrack, true); + } + + @Override + public void noMatches() { + logger.warn("Track not found: "+trackUrl); + } + + @Override + public void loadFailed(FriendlyException exception) { + logger.error("Cant load media!"); + logger.error(exception.getMessage()); + } + }); + } + + /** * Load playlist to playlist * @param playlist Loaded playlist @@ -270,7 +302,7 @@ public class AudioM { */ public void add(MessageReceivedEvent event, String url, int playListLimit, boolean onHead) { if(playedChanel != null){ - loadAndPlay(event,playedChanel, url, playListLimit,onHead); + loadAndPlay(event ,playedChanel, url, playListLimit,onHead); } else { @@ -320,6 +352,7 @@ public class AudioM { public AudioPlayerManager getPlayerManager() { return playerManager; } + public VoiceChannel getPlayedChanel() { return playedChanel; } diff --git a/src/main/java/net/Broken/audio/TrackScheduler.java b/src/main/java/net/Broken/audio/TrackScheduler.java index 08a96eb..5596fba 100644 --- a/src/main/java/net/Broken/audio/TrackScheduler.java +++ b/src/main/java/net/Broken/audio/TrackScheduler.java @@ -1,14 +1,20 @@ package net.Broken.audio; +import com.google.api.client.googleapis.json.GoogleJsonResponseException; import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; +import net.Broken.Commands.Music; +import net.Broken.MainBot; import net.Broken.RestApi.Data.UserAudioTrackData; +import net.Broken.audio.Youtube.YoutubeTools; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingDeque; @@ -21,6 +27,7 @@ public class TrackScheduler extends AudioEventAdapter { private final AudioPlayer player; private final BlockingDeque queue; private UserAudioTrack currentPlayingTrack; + private boolean autoPlay = true; Logger logger = LogManager.getLogger(); /** @@ -48,6 +55,9 @@ public class TrackScheduler extends AudioEventAdapter { else{ currentPlayingTrack = track; } + needAutoPlay(); + + } /** @@ -64,6 +74,7 @@ public class TrackScheduler extends AudioEventAdapter { else{ currentPlayingTrack = track; } + needAutoPlay(); } @@ -120,6 +131,7 @@ public class TrackScheduler extends AudioEventAdapter { } } logger.info("Delete failure! Not found."); + needAutoPlay(); return false; } @@ -133,6 +145,7 @@ public class TrackScheduler extends AudioEventAdapter { this.currentPlayingTrack = track; if(track != null) player.startTrack(track.getAudioTrack(), false); + needAutoPlay(); } @Override @@ -140,6 +153,26 @@ public class TrackScheduler extends AudioEventAdapter { // Only start the next track if the end reason is suitable for it (FINISHED or LOAD_FAILED) if (endReason.mayStartNext) { nextTrack(); + needAutoPlay(); + } + } + + private void needAutoPlay(){ + if((queue.size() < 1) && autoPlay && currentPlayingTrack != null){ + logger.info("Auto add needed!"); + AudioM audioM = AudioM.getInstance(null); + YoutubeTools youtubeTools = YoutubeTools.getInstance(null); + try { + String id = youtubeTools.getRelatedVideo(currentPlayingTrack.getAudioTrack().getInfo().identifier); + logger.info("Related id: "+id); + audioM.loadAndPlayAuto(id); + + } catch (GoogleJsonResponseException e) { + logger.error("There was a service error: " + e.getDetails().getCode() + " : " + e.getDetails().getMessage()); + } catch (Throwable t) { + logger.catching(t); + } + } } diff --git a/src/main/java/net/Broken/audio/WebLoadUtils.java b/src/main/java/net/Broken/audio/WebLoadUtils.java index 8608d6a..e1cdb95 100644 --- a/src/main/java/net/Broken/audio/WebLoadUtils.java +++ b/src/main/java/net/Broken/audio/WebLoadUtils.java @@ -31,7 +31,7 @@ public class WebLoadUtils { AudioPlayerManager playerM = musicCommand.getAudioManager().getPlayerManager(); try { - AudioM audioM = musicCommand.getAudioManager(); + AudioM audioM = AudioM.getInstance(null); playerM.loadItemOrdered(musicCommand.getAudioManager().getGuildMusicManager(), data.url, new AudioLoadResultHandler() { @Override public void trackLoaded(AudioTrack track) { diff --git a/src/main/java/net/Broken/audio/Youtube/Autorization.java b/src/main/java/net/Broken/audio/Youtube/Autorization.java new file mode 100644 index 0000000..8ae3531 --- /dev/null +++ b/src/main/java/net/Broken/audio/Youtube/Autorization.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package net.Broken.audio.Youtube; +import com.google.api.client.auth.oauth2.AuthorizationCodeFlow; +import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl; +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.auth.oauth2.TokenResponse; +import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; +import com.google.api.client.extensions.java6.auth.oauth2.VerificationCodeReceiver; +import com.google.api.client.util.Preconditions; +import net.Broken.Tools.PrivateMessage; +import net.dv8tion.jda.core.entities.Guild; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.*; +import java.io.IOException; + + +/** + * OAuth 2.0 authorization code flow for an installed Java application that persists end-user + * credentials. + * + *

+ * Implementation is thread-safe. + *

+ * + * @since 1.11 + * @author Yaniv Inbar + */ +public class Autorization { + + /** Authorization code flow. */ + private final AuthorizationCodeFlow flow; + + /** Verification code receiver. */ + private final VerificationCodeReceiver receiver; + + private static final Logger LOGGER = LogManager.getLogger(); + + private Guild guild; + /** + * @param flow authorization code flow + * @param receiver verification code receiver + */ + public Autorization(AuthorizationCodeFlow flow, VerificationCodeReceiver receiver, Guild guild) { + this.flow = Preconditions.checkNotNull(flow); + this.receiver = Preconditions.checkNotNull(receiver); + this.guild = guild; + } + + /** + * Authorizes the installed application to access user's protected data. + * + * @param userId user ID or {@code null} if not using a persisted credential store + * @return credential + */ + public Credential authorize(String userId) throws IOException { + try { + Credential credential = flow.loadCredential(userId); + if (credential != null + && (credential.getRefreshToken() != null || + credential.getExpiresInSeconds() == null || + credential.getExpiresInSeconds() > 60)) { + return credential; + } + // open in browser + String redirectUri = receiver.getRedirectUri(); + AuthorizationCodeRequestUrl authorizationUrl = + flow.newAuthorizationUrl().setRedirectUri(redirectUri); + onAuthorization(authorizationUrl); + // receive authorization code and exchange it for an access token + String code = receiver.waitForCode(); + TokenResponse response = flow.newTokenRequest(code).setRedirectUri(redirectUri).execute(); + // store credential and return it + return flow.createAndStoreCredential(response, userId); + } finally { + receiver.stop(); + } + } + + /** + * Handles user authorization by redirecting to the OAuth 2.0 authorization server. + * + *

+ * Default implementation is to call {@code browse(authorizationUrl.build())}. Subclasses may + * override to provide optional parameters such as the recommended state parameter. Sample + * implementation: + *

+ * + *
+     @Override
+     protected void onAuthorization(AuthorizationCodeRequestUrl authorizationUrl) throws IOException {
+     authorizationUrl.setState("xyz");
+     super.onAuthorization(authorizationUrl);
+     }
+     * 
+ * + * @param authorizationUrl authorization URL + * @throws IOException I/O exception + */ + protected void onAuthorization(AuthorizationCodeRequestUrl authorizationUrl) throws IOException { + browse(authorizationUrl.build()); + } + + /** + * Open a browser at the given URL using {@link Desktop} if available, or alternatively output the + * URL to {@link System#out} for command-line applications. + * + * @param url URL to browse + */ + public void browse(String url) { + Preconditions.checkNotNull(url); + // Ask user to open in their browser using copy-paste + LOGGER.fatal("Please open this URL: "+url); + PrivateMessage.send(guild.getOwner().getUser(),"Please open this url to confirm google api account acces : " + url,null); + + } + + /** Returns the authorization code flow. */ + public final AuthorizationCodeFlow getFlow() { + return flow; + } + + /** Returns the verification code receiver. */ + public final VerificationCodeReceiver getReceiver() { + return receiver; + } +} diff --git a/src/main/java/net/Broken/audio/Youtube/YoutubeTools.java b/src/main/java/net/Broken/audio/Youtube/YoutubeTools.java new file mode 100644 index 0000000..6aeaf44 --- /dev/null +++ b/src/main/java/net/Broken/audio/Youtube/YoutubeTools.java @@ -0,0 +1,135 @@ +package net.Broken.audio.Youtube; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; +import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; +import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.Json; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.store.FileDataStoreFactory; +import com.google.api.services.youtube.YouTube; +import com.google.api.services.youtube.YouTubeScopes; +import com.google.api.services.youtube.model.SearchListResponse; +import net.dv8tion.jda.core.entities.Guild; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; + +public class YoutubeTools { + /** Application name. */ + private final String APPLICATION_NAME = "Discord Bot"; + + /** Directory to store user credentials for this application. */ + private final File DATA_STORE_DIR = new File(".credentials/java-youtube-api"); + private final File CLIENT_SECRET_DIR = new File(".credentials/client_secret.json"); + + /** Global instance of the {@link FileDataStoreFactory}. */ + private FileDataStoreFactory DATA_STORE_FACTORY; + + /** Global instance of the JSON factory. */ + private final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); + + /** Global instance of the HTTP transport. */ + private HttpTransport HTTP_TRANSPORT; + + private Logger logger = LogManager.getLogger(); + + private Guild guild; + + /** Global instance of the scopes required by this quickstart. + * + * If modifying these scopes, delete your previously saved credentials + * at ~/.credentials/drive-java-quickstart + */ + private final Collection SCOPES = Arrays.asList(YouTubeScopes.YOUTUBEPARTNER, YouTubeScopes.YOUTUBE_FORCE_SSL); + + private static YoutubeTools INSTANCE ; + + private YoutubeTools(Guild guild){ + + try { + HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); + DATA_STORE_FACTORY = new FileDataStoreFactory(DATA_STORE_DIR); + } catch (Throwable t) { + logger.catching(t); + } + this.guild = guild; + } + + public static YoutubeTools getInstance(Guild guild){ + if(INSTANCE == null) + INSTANCE = new YoutubeTools(guild); + return INSTANCE; + } + + + + /** + * Creates an authorized Credential object. + * @return an authorized Credential object. + * @throws IOException + */ + private Credential authorize() throws IOException { + // Load client secrets. + InputStream in = new FileInputStream(CLIENT_SECRET_DIR); + GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader( in )); + + + // Build flow and trigger user authorization request. + GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( + HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES) + .setDataStoreFactory(DATA_STORE_FACTORY) + .setAccessType("offline") + .build(); + Credential credential = new Autorization(flow, new LocalServerReceiver(), guild).authorize("user"); + logger.debug("Credentials saved to " + DATA_STORE_DIR.getAbsolutePath()); + return credential; + } + + /** + * Build and return an authorized API client service, such as a YouTube + * Data API client service. + * @return an authorized API client service + * @throws IOException + */ + public YouTube getYouTubeService() throws IOException { + Credential credential = authorize(); + return new YouTube.Builder( + HTTP_TRANSPORT, JSON_FACTORY, credential) + .setApplicationName(APPLICATION_NAME) + .build(); + } + + public String getRelatedVideo(String videoId) throws IOException, GoogleJsonResponseException, Throwable { + + YouTube youtube = getYouTubeService(); + + HashMap parameters = new HashMap<>(); + parameters.put("part", "snippet"); + parameters.put("relatedToVideoId", videoId); + parameters.put("type", "video"); + + YouTube.Search.List searchListRelatedVideosRequest = youtube.search().list(parameters.get("part").toString()); + if (parameters.containsKey("relatedToVideoId") && parameters.get("relatedToVideoId") != "") { + searchListRelatedVideosRequest.setRelatedToVideoId(parameters.get("relatedToVideoId").toString()); + } + + if (parameters.containsKey("type") && parameters.get("type") != "") { + searchListRelatedVideosRequest.setType(parameters.get("type").toString()); + } + + SearchListResponse response = searchListRelatedVideosRequest.execute(); + + return response.getItems().get(0).getId().getVideoId(); + + } +}