diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java index 47b087d..b370226 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java @@ -1,15 +1,28 @@ package de.b00tload.tools.lastfmtospotifyplaylist; +import com.neovisionaries.i18n.CountryCode; import de.b00tload.tools.lastfmtospotifyplaylist.arguments.ArgumentHandler; import de.b00tload.tools.lastfmtospotifyplaylist.arguments.Arguments; import de.b00tload.tools.lastfmtospotifyplaylist.util.PeriodHelper; +import de.b00tload.tools.lastfmtospotifyplaylist.util.TimeHelper; +import de.b00tload.tools.lastfmtospotifyplaylist.util.TokenHelper; import de.umass.lastfm.Caller; import de.umass.lastfm.Track; import de.umass.lastfm.User; +import io.javalin.Javalin; +import io.javalin.http.ContentType; +import io.javalin.http.HttpStatus; +import se.michaelthelin.spotify.SpotifyApi; +import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials; +import se.michaelthelin.spotify.model_objects.specification.Playlist; -import java.util.Collection; -import java.util.HashMap; +import java.net.URI; +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import static de.b00tload.tools.lastfmtospotifyplaylist.util.Logger.logLn; @@ -19,9 +32,10 @@ public class LastFMToSpotify { public static HashMap configuration; public static void main(String[] args) { - // create hash map with user agent + // create hash map with user agent and default playlist name configuration = new HashMap<>(); configuration.put("requests.useragent", "LastFMToSpotify/1.0-Snapshot (" + System.getProperty("os.name") + "; " + System.getProperty("os.arch") + ") Java/" + System.getProperty("java.version")); + configuration.put("playlist.name", "LastFMToSpotify@" + LocalDateTime.now(Clock.systemDefaultZone()).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); // parse arguments for (int a = 0; a < args.length; a++) { Arguments arg; @@ -45,32 +59,73 @@ public class LastFMToSpotify { } } - - // Start Progress Bar try { logLn("Authenticating with Spotify...", 1); + SpotifyApi.Builder build = SpotifyApi.builder(); + build.setClientId(configuration.get("spotify.clientid")); + build.setClientSecret(configuration.get("spotify.secret")); + build.setRedirectUri(URI.create("http://localhost:9876/callback/spotify/")); + SpotifyApi api = build.build(); + AtomicBoolean waiting = new AtomicBoolean(true); + try (Javalin webserver = Javalin.create().start(9876)) { + Runtime.getRuntime().addShutdownHook(new Thread(webserver::stop)); + webserver.get("/callback/spotify", ctx -> { + if(ctx.queryParamMap().containsKey("code")) { + AuthorizationCodeCredentials cred = api.authorizationCode(ctx.queryParam("code")).build().execute(); + configuration.put("spotify.access", cred.getAccessToken()); + if(configuration.containsKey("spotify.saveaccess")) TokenHelper.saveTokens(cred); + ctx.result("success. ").contentType(ContentType.TEXT_HTML).status(HttpStatus.OK); + waiting.set(false); + } else { + logLn("Error: Spotify authorization failed."+LINE_SEPERATOR+ctx.queryParam("error"), 1); + System.exit(500); + } + }); + logLn("Waiting for Spotify authorization.", 1); + //TODO: Open auth page in Browser + while (waiting.get()); + } logLn("Authenticating with LastFM...", 1); + Caller.getInstance().setApiRootUrl("https://ws.audioscrobbler.com/2.0/"); Caller.getInstance().setUserAgent(configuration.get("requests.useragent")); logLn(User.getInfo(configuration.get("lastfm.user"), configuration.get("lastfm.apikey")).getName(), 1); logLn("Reading from LastFM...", 1); Collection tracks = User.getTopTracks(configuration.get("lastfm.user"), PeriodHelper.getPeriodByString(configuration.get("lastfm.period")), configuration.get("lastfm.apikey")); - for (Track track : tracks) { - logLn(track.getName() + " by " + track.getArtist(), 3); - } logLn("Creating Playlist...", 1); - //SpotifyApi.Builder build = SpotifyApi.builder(); - //build.setClientId(configuration.get("spotify.clientid")); - //build.setClientSecret(configuration.get("spotify.secret")); - //build.setRedirectUri(URI.create("http://localhost:9876/callback/spotify/")); - //SpotifyApi api = build.build(); - //api.setAccessToken(configuration.get("spotify.access")); - //api.createPlaylist(api.getCurrentUsersProfile().build().execute().getId(), configuration.get("playlist.name")).setHeader("User-Agent", configuration.get("requests.useragent")); + api.setAccessToken(configuration.get("spotify.access")); + Playlist list = api.createPlaylist(api.getCurrentUsersProfile().build().execute().getId(), configuration.get("playlist.name")).public_(configuration.containsKey("playlist.public")).collaborative(configuration.containsKey("playlist.collab")).setHeader("User-Agent", configuration.get("requests.useragent")).build().execute(); + List adders = new LinkedList<>(); + String charsToReplace = "[\"']"; //regex for " and ' + for (Track track : tracks) { + logLn("Adding " + track.getName() + " by " + track.getArtist(), 3); + StringBuilder searchQuery = new StringBuilder(); + searchQuery.append("track:").append(track.getName().replaceAll(charsToReplace, "")); + searchQuery.append(" artist:").append(track.getArtist()); + if(track.getAlbum()!=null&&!track.getAlbum().equalsIgnoreCase("null")&&!track.getAlbum().isEmpty()) + searchQuery.append(" album:").append(track.getAlbum()); + logLn("Search query: " + searchQuery, 3); + se.michaelthelin.spotify.model_objects.specification.Track[] add = api.searchTracks(searchQuery.toString()).market(CountryCode.DE).setHeader("User-Agent", configuration.get("requests.useragent")).build().execute().getItems(); + if(add.length!=0) { +// adders.add(add[0].getUri()); +// logLn("Added " + add[0].getName() + " to " + configuration.get("playlist.name"), 3); + for(se.michaelthelin.spotify.model_objects.specification.Track t : add){ + if(t.getName().equalsIgnoreCase(track.getName())){ + adders.add(t.getUri()); + logLn("Added " + add[0].getName() + " to " + configuration.get("playlist.name"), 3); + break; + } + } + } + } + api.addItemsToPlaylist(list.getId(), adders.toArray(String[]::new)).build().execute(); + if(configuration.containsKey("playlist.cover")){ + logLn("Check for \"null\" if setting cover was successful: " + api.uploadCustomPlaylistCoverImage(list.getId()).image_data(configuration.get("playlist.cover")).setHeader("User-Agent", configuration.get("requests.useragent")).build().execute(),3); + } logLn("Done.", 1); // } catch (IOException | ParseException | SpotifyWebApiException e) { } catch (Exception e) { throw new RuntimeException(e); } - //TODO: Implement } } diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java index aa8ef72..c533f1f 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java @@ -1,10 +1,26 @@ package de.b00tload.tools.lastfmtospotifyplaylist.arguments; +import de.b00tload.tools.lastfmtospotifyplaylist.util.FileHelper; +import de.b00tload.tools.lastfmtospotifyplaylist.util.TimeHelper; import de.umass.lastfm.Period; import org.jetbrains.annotations.Nullable; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.TextStyle; +import java.time.temporal.ChronoField; +import java.time.temporal.IsoFields; +import java.time.temporal.TemporalField; +import java.time.temporal.WeekFields; +import java.util.Locale; + import static de.b00tload.tools.lastfmtospotifyplaylist.LastFMToSpotify.LINE_SEPERATOR; import static de.b00tload.tools.lastfmtospotifyplaylist.LastFMToSpotify.configuration; +import static de.b00tload.tools.lastfmtospotifyplaylist.util.Logger.logLn; public class ArgumentHandler { @@ -21,6 +37,10 @@ public class ArgumentHandler { case QUARTERLY -> period(Period.THREE_MONTHS); case BIANNUALLY -> period(Period.SIX_MONTHS); case YEARLY -> period(Period.TWELVE_MONTHS); + case COVER -> cover(value); + case NAME -> name(value); + case PUBLIC -> access("public"); + case COLLABORATIVE -> access("collaborative"); } } @@ -111,4 +131,59 @@ public class ArgumentHandler { private static void period(Period value) { configuration.put("lastfm.period", value.getString()); } + + private static void cover(String value) { + if (value == null || value.equalsIgnoreCase("") || !Files.exists(Path.of(value.replace("\\", "//")))) { + System.out.println("--coverart must be provided with a path to a png file. Check usage: " + Arguments.COVER.getUsage()); + System.exit(500); + } + String base64 = FileHelper.encodeFileToBase64(new File(value.replace("\\", "//"))); + configuration.put("playlist.cover", base64); + } + + private static void access(String value) { + switch (value) { + case "collaborative" -> configuration.put("playlist.collab", "collab"); + case "public" -> configuration.put("playlist.public", "public"); + } + } + + private static void name(String value) { + if (value == null || value.equalsIgnoreCase("")) { + System.out.println("--playlistname must be provided with a playlist name. Check usage: " + Arguments.NAME.getUsage()); + System.exit(500); + } + LocalDateTime now = LocalDateTime.now(Clock.systemDefaultZone()); + Locale loc = Locale.forLanguageTag(System.getProperty("user.country")); + if(value.matches("(%\\$-?\\d*\\$).*")){ + int offsetDays = Integer.parseInt(value.substring(2).split("\\$")[0]); + now = offsetDays < 0 ? now.minusDays(Math.abs(offsetDays)) : now.plusDays(Math.abs(offsetDays)); + } + String name = value.replace("%YYYY", String.valueOf(now.getYear())).replace("%YY", String.valueOf(now.getYear()).substring(2)) + .replace("%MMMM", now.getMonth().name().charAt(0) + now.getMonth().name().toLowerCase().substring(1)) + .replace("%MMM", now.getMonth().getDisplayName(TextStyle.FULL, loc)) + .replace("%MM", (String.valueOf(now.getMonth().getValue()).length() == 1 ? "0" + now.getMonth().getValue() : String.valueOf(now.getMonth().getValue()))) + .replace("%M", String.valueOf(now.getMonth().getValue())) + .replace("%DD", (String.valueOf(now.getDayOfMonth()).length() == 1 ? "0" + now.getDayOfMonth() : String.valueOf(now.getDayOfMonth()))) + .replace("%D", String.valueOf(now.getDayOfMonth())) + .replace("%DDDD", now.getDayOfWeek().getDisplayName(TextStyle.FULL, loc)) + .replace("%DDD", now.getDayOfWeek().getDisplayName(TextStyle.SHORT, loc)) + .replace("%WW", (String.valueOf(now.get(WeekFields.of(loc).weekOfWeekBasedYear())).length() == 1 ? "0" + now.get(WeekFields.of(loc).weekOfWeekBasedYear()) : String.valueOf(now.get(WeekFields.of(loc).weekOfWeekBasedYear())))) + .replace("%W", String.valueOf(now.get(WeekFields.of(loc).weekOfWeekBasedYear()))) + .replace("%HH", (String.valueOf(now.get(ChronoField.HOUR_OF_DAY)).length() == 1 ? "0" + now.get(ChronoField.HOUR_OF_DAY) : String.valueOf(now.get(ChronoField.HOUR_OF_DAY)))) + .replace("%H", String.valueOf(now.get(ChronoField.HOUR_OF_DAY))) + .replace("%hh", (String.valueOf(now.get(ChronoField.HOUR_OF_AMPM)).length() == 1 ? "0" + now.get(ChronoField.HOUR_OF_AMPM) : String.valueOf(now.get(ChronoField.HOUR_OF_AMPM)))) + .replace("%h", String.valueOf(now.get(ChronoField.HOUR_OF_AMPM))) + .replace("%P", now.get(ChronoField.AMPM_OF_DAY)==0 ? "AM" : "PM") + .replace("%p", now.get(ChronoField.AMPM_OF_DAY)==0 ? "am" : "pm") + .replace("%mm", (String.valueOf(now.getMinute()).length() == 1 ? "0" + now.getMinute() : String.valueOf(now.getMinute()))) + .replace("%m", String.valueOf(now.getMinute())) + .replace("%ss", (String.valueOf(now.getSecond()).length() == 1 ? "0" + now.getSecond() : String.valueOf(now.getSecond()))) + .replace("%s", String.valueOf(now.getSecond())) + .replace("%o", TimeHelper.getUTCOffset(now)) + .replaceAll("%\\$-?\\d*\\$", "") + .replace("%%", "%"); + + configuration.put("playlist.name", name); + } } diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java index c8fcd00..f6009b2 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java @@ -24,16 +24,24 @@ public enum Arguments { + "Sets the LastFM API token.", "--lastfmtoken ", "lT", "lToken"), USER("lastfmuser", "[Required]" + LINE_SEPERATOR + "Sets the LastFM API token.", "--lastfmuser ", "lU", "lUser"), - WEEKLY("weekly", "[Optional]" + LINE_SEPERATOR + WEEKLY("weekly", "[Optional] [EXCLUSIVE: weekly, monthly, quarterly, biannually, annually]" + LINE_SEPERATOR + "Creates a playlist from your top tracks from last week.", "--weekly", "W"), - MONTHLY("monthly", "[Optional, Default]" + LINE_SEPERATOR + MONTHLY("monthly", "[Optional, Default] [EXCLUSIVE: weekly, monthly, quarterly, biannually, annually]" + LINE_SEPERATOR + "Creates a playlist from your top tracks from last month.", "--monthly", "M"), - QUARTERLY("quarterly", "[Optional]" + LINE_SEPERATOR + QUARTERLY("quarterly", "[Optional] [EXCLUSIVE: weekly, monthly, quarterly, biannually, annually]" + LINE_SEPERATOR + "Creates a playlist from your top tracks from last quarter.", "--quarterly", "Q"), - BIANNUALLY("biannually", "[Optional]" + LINE_SEPERATOR + BIANNUALLY("biannually", "[Optional] [EXCLUSIVE: weekly, monthly, quarterly, biannually, annually]" + LINE_SEPERATOR + "Creates a playlist from your top tracks from last half-year.", "--biannualy", "B"), - YEARLY("yearly", "[Optional]" + LINE_SEPERATOR - + "Creates a playlist from your top tracks from last year.", "--anually", "A");; + YEARLY("annually", "[Optional] [EXCLUSIVE: weekly, monthly, quarterly, biannually, annually]" + LINE_SEPERATOR + + "Creates a playlist from your top tracks from last year.", "--anually", "A"), + COVER("coverart", "[Optional]" + LINE_SEPERATOR + + "Will set a cover art for the playlist. Must be jpeg/jpg.", "--coverart ", "ca", "cover"), + NAME("playlistname", "[Optional]" + LINE_SEPERATOR + + "Sets the playlist name. Supports templating. Refer to https://github.com/B00tLoad/LastFMtoSpotifyPlaylist/wiki/Filename-Templating.", "--playlistname ", "pName", "pN"), + PUBLIC("public", "[Optional] [EXCLUSIVE: public, collaborative]" + LINE_SEPERATOR + + "Makes the playlist public.", "--public", "pP"), + COLLABORATIVE("collaborative", "[Optional] [EXCLUSIVE: public, collaborative]" + LINE_SEPERATOR + + "Makes the playlist collaborative.", "--collaborative", "pC"); private final String name; private final String description; diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/FileHelper.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/FileHelper.java new file mode 100644 index 0000000..e1ad05b --- /dev/null +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/FileHelper.java @@ -0,0 +1,23 @@ +package de.b00tload.tools.lastfmtospotifyplaylist.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Base64; + +public class FileHelper { + + public static String encodeFileToBase64(File file){ + String encodedfile = null; + try (FileInputStream fileInputStreamReader = new FileInputStream(file)){ + byte[] bytes = new byte[(int)file.length()]; + fileInputStreamReader.read(bytes); + encodedfile = Base64.getEncoder().encodeToString(bytes); + } catch (IOException e) { + e.printStackTrace(); + } + + return encodedfile; + } + +} diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TimeHelper.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TimeHelper.java new file mode 100644 index 0000000..f46599c --- /dev/null +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TimeHelper.java @@ -0,0 +1,26 @@ +package de.b00tload.tools.lastfmtospotifyplaylist.util; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; + +public class TimeHelper { + + public static int getUTCOffsetHours(LocalDateTime now){ + + return (int) Math.floor((double) now.atZone(ZoneId.systemDefault()).getOffset().getTotalSeconds()/3600); + } + + public static int getUTCOffsetMinutes(LocalDateTime now){ + return (now.atZone(ZoneId.systemDefault()).getOffset().getTotalSeconds()/60); + } + + public static String getUTCOffset(LocalDateTime now){ + int hour = getUTCOffsetHours(now); + String h = (hour == Math.abs(hour) ? "+" : "-") + (String.valueOf(Math.abs(hour)).length() == 1 ? "0" + Math.abs(hour) : String.valueOf(Math.abs(hour))); + int min = Math.abs(getUTCOffsetMinutes(now))-(Math.abs(hour)*60); + String m = (String.valueOf(Math.abs(min)).length() == 1 ? "0" + Math.abs(min) : String.valueOf(Math.abs(min))); + return h + ":" + m; + } + +} diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TokenHelper.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TokenHelper.java new file mode 100644 index 0000000..8dfd3d2 --- /dev/null +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TokenHelper.java @@ -0,0 +1,19 @@ +package de.b00tload.tools.lastfmtospotifyplaylist.util; + +import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials; + +public class TokenHelper { + public static void saveTokens(AuthorizationCodeCredentials cred) { + //TODO: Save tokens + } + + public static String getAccessToken(){ + //TODO: Read AccessToken + return ""; + } + + public static String getRefreshToken(){ + //TODO: Read RefreshToken + return ""; + } +}