Merge pull request #9 from B00tLoad/SpotifyPlaylistBuilder
Implemented Playlist builder
This commit was merged in pull request #9.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user