Merge pull request #9 from B00tLoad/SpotifyPlaylistBuilder

Implemented Playlist builder
This commit was merged in pull request #9.
This commit is contained in:
Alix von Schirp
2023-01-24 15:02:12 +01:00
committed by GitHub
6 changed files with 228 additions and 22 deletions

View File

@@ -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<String, String> 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. <script>let win = window.open(null, '_self');win.close();</script>").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<Track> 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<String> 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
}
}

View File

@@ -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);
}
}

View File

@@ -24,16 +24,24 @@ public enum Arguments {
+ "Sets the LastFM API token.", "--lastfmtoken <apitoken>", "lT", "lToken"),
USER("lastfmuser", "[Required]" + LINE_SEPERATOR
+ "Sets the LastFM API token.", "--lastfmuser <username>", "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 <path/to/coverart.jpg>", "ca", "cover"),
NAME("playlistname", "[Optional]" + LINE_SEPERATOR
+ "Sets the playlist name. Supports templating. Refer to https://github.com/B00tLoad/LastFMtoSpotifyPlaylist/wiki/Filename-Templating.", "--playlistname <name>", "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;

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 "";
}
}