From 011cdc1481455e7d928e813a951e1ebae1e4a0e2 Mon Sep 17 00:00:00 2001 From: Alix von Schirp Date: Sat, 21 Jan 2023 02:24:52 +0100 Subject: [PATCH 1/5] Implemented adding songs to playlist \+ Creation of playlist with default name \+ implemented track search (opens #2) \+ Implemented adding tracks to list \+ Implemented setting custom playlist cover - Missing ability to select file to upload as cover (opens #3) - Missing saving and retrieving auth tokens for use in chron jobs + flag to enable (opens #4) - Known issue: some tracks cannot be found, although they were played on spotify to be scrobbled onto lastfm (see #2) - Missing flag to set a custom playlist name (opens #5) - missing flags to make playlists public and/or collaborative on creation. (opens #6) --- .../LastFMToSpotify.java | 70 +++++++++++++++---- .../util/TokenHelper.java | 19 +++++ 2 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TokenHelper.java diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java index 47b087d..48eacde 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java @@ -1,15 +1,26 @@ 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.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 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; @@ -22,6 +33,7 @@ public class LastFMToSpotify { // create hash map with user agent 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; @@ -49,28 +61,60 @@ 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)) { + 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); + 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()); + webserver.stop(); + } logLn("Authenticating with LastFM...", 1); 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")||configuration.containsKey("playlist.collab")).collaborative(configuration.containsKey("playlist.collab")).setHeader("User-Agent", configuration.get("requests.useragent")).build().execute(); + List adders = new LinkedList<>(); + for (Track track : tracks) { + logLn("Adding " + track.getName() + " by " + track.getArtist(), 3); + StringBuilder searchQuery = new StringBuilder(); + searchQuery.append("track:").append(track.getName()); + 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.toString(), 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); + } + } + api.addItemsToPlaylist(list.getId(), adders.toArray(String[]::new)).build().execute(); + if(configuration.containsKey(configuration.get("playlist.cover"))) api.uploadCustomPlaylistCoverImage(list.getId()).image_data(configuration.get("playlist.cover")).build().execute(); 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/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 ""; + } +} -- 2.49.1 From ce9f8d127e23c18ff45df9f1ce821afb4fda675f Mon Sep 17 00:00:00 2001 From: Alix von Schirp Date: Sun, 22 Jan 2023 19:27:37 +0100 Subject: [PATCH 2/5] Fixing #2 and doing a bit of clean up - Regex-replacing ' and " in song titles (fixes #2) - found problem where the webserver receives the auth data from Spotify but never sends out a response. (opens #7) - gracefully stopping webserver at the end of the process - added functionality for the auth tab to be self-closing - set the LastFM api wrapper to use https instead of http --- .../lastfmtospotifyplaylist/LastFMToSpotify.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java index 48eacde..0a28505 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java @@ -11,6 +11,7 @@ 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; @@ -57,8 +58,6 @@ public class LastFMToSpotify { } } - - // Start Progress Bar try { logLn("Authenticating with Spotify...", 1); SpotifyApi.Builder build = SpotifyApi.builder(); @@ -68,12 +67,13 @@ public class LastFMToSpotify { 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); + 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); @@ -83,9 +83,9 @@ public class LastFMToSpotify { logLn("Waiting for Spotify authorization.", 1); //TODO: Open auth page in Browser while (waiting.get()); - webserver.stop(); } 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); @@ -94,14 +94,15 @@ public class LastFMToSpotify { api.setAccessToken(configuration.get("spotify.access")); Playlist list = api.createPlaylist(api.getCurrentUsersProfile().build().execute().getId(), configuration.get("playlist.name")).public_(configuration.containsKey("playlist.public")||configuration.containsKey("playlist.collab")).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()); + 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.toString(), 3); + 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()); -- 2.49.1 From b7830190c6ce7d18516b324d2637641178eb3611 Mon Sep 17 00:00:00 2001 From: Alix von Schirp Date: Mon, 23 Jan 2023 21:41:46 +0100 Subject: [PATCH 3/5] Cover upload implemented closes #3 --- .../LastFMToSpotify.java | 4 +++- .../arguments/ArgumentHandler.java | 16 +++++++++++++ .../arguments/Arguments.java | 4 +++- .../util/FileHelper.java | 23 +++++++++++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/FileHelper.java diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java index 0a28505..ff1d2da 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java @@ -110,7 +110,9 @@ public class LastFMToSpotify { } } api.addItemsToPlaylist(list.getId(), adders.toArray(String[]::new)).build().execute(); - if(configuration.containsKey(configuration.get("playlist.cover"))) api.uploadCustomPlaylistCoverImage(list.getId()).image_data(configuration.get("playlist.cover")).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) { 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..170ea2e 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,16 @@ package de.b00tload.tools.lastfmtospotifyplaylist.arguments; +import de.b00tload.tools.lastfmtospotifyplaylist.util.FileHelper; 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 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 +27,7 @@ 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); } } @@ -111,4 +118,13 @@ 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); + } } 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..2dfa0d9 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java @@ -33,7 +33,9 @@ public enum Arguments { BIANNUALLY("biannually", "[Optional]" + 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");; + + "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"); 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; + } + +} -- 2.49.1 From cadb4a98b34c5f27c344ad0a1ca7e50263caa26a Mon Sep 17 00:00:00 2001 From: Alix von Schirp Date: Tue, 24 Jan 2023 04:40:04 +0100 Subject: [PATCH 4/5] Implemented Playlist naming closes #5 Also did a minor fix to the playlist building system, where sometimes a different version of the track was selected. Now Track titles have to match. --- .../LastFMToSpotify.java | 14 ++++-- .../arguments/ArgumentHandler.java | 50 +++++++++++++++++++ .../arguments/Arguments.java | 4 +- .../util/TimeHelper.java | 26 ++++++++++ 4 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/util/TimeHelper.java diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java index ff1d2da..ee5306f 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java @@ -5,6 +5,7 @@ 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; @@ -31,7 +32,7 @@ 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)); @@ -105,8 +106,15 @@ public class LastFMToSpotify { 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); +// 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(); 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 170ea2e..111f39f 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java @@ -1,12 +1,22 @@ 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; @@ -28,6 +38,7 @@ public class ArgumentHandler { case BIANNUALLY -> period(Period.SIX_MONTHS); case YEARLY -> period(Period.TWELVE_MONTHS); case COVER -> cover(value); + case NAME -> name(value); } } @@ -127,4 +138,43 @@ public class ArgumentHandler { String base64 = FileHelper.encodeFileToBase64(new File(value.replace("\\", "//"))); configuration.put("playlist.cover", base64); } + + 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 2dfa0d9..4e91046 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/Arguments.java @@ -35,7 +35,9 @@ public enum Arguments { YEARLY("yearly", "[Optional]" + 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"); + + "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"); private final String name; private final String description; 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; + } + +} -- 2.49.1 From 978640ba105b3619a5b5595ab2d3197a6c3ef6a5 Mon Sep 17 00:00:00 2001 From: Alix von Schirp Date: Tue, 24 Jan 2023 04:59:40 +0100 Subject: [PATCH 5/5] Implemented public / collaborative flag closes #6 also stated flag exclusivity in argument descriptions --- .../lastfmtospotifyplaylist/LastFMToSpotify.java | 2 +- .../arguments/ArgumentHandler.java | 9 +++++++++ .../arguments/Arguments.java | 16 ++++++++++------ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java index ee5306f..b370226 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/LastFMToSpotify.java @@ -93,7 +93,7 @@ public class LastFMToSpotify { Collection tracks = User.getTopTracks(configuration.get("lastfm.user"), PeriodHelper.getPeriodByString(configuration.get("lastfm.period")), configuration.get("lastfm.apikey")); logLn("Creating Playlist...", 1); api.setAccessToken(configuration.get("spotify.access")); - Playlist list = api.createPlaylist(api.getCurrentUsersProfile().build().execute().getId(), configuration.get("playlist.name")).public_(configuration.containsKey("playlist.public")||configuration.containsKey("playlist.collab")).collaborative(configuration.containsKey("playlist.collab")).setHeader("User-Agent", configuration.get("requests.useragent")).build().execute(); + 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) { 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 111f39f..c533f1f 100644 --- a/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java +++ b/src/main/java/de/b00tload/tools/lastfmtospotifyplaylist/arguments/ArgumentHandler.java @@ -39,6 +39,8 @@ public class ArgumentHandler { case YEARLY -> period(Period.TWELVE_MONTHS); case COVER -> cover(value); case NAME -> name(value); + case PUBLIC -> access("public"); + case COLLABORATIVE -> access("collaborative"); } } @@ -139,6 +141,13 @@ public class ArgumentHandler { 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()); 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 4e91046..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,20 +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 + 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"); + + "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; -- 2.49.1