Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Playlist builder #9

Merged
merged 5 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
Expand All @@ -45,32 +59,73 @@ public static void main(String[] args) {
}
}


// 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"));
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")).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(track.getName() + " by " + track.getArtist(), 3);
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("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"));
logLn("Done.", 1);
// } catch (IOException | ParseException | SpotifyWebApiException e) {
} catch (Exception e) {
throw new RuntimeException(e);
}
//TODO: Implement
}

}
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -21,6 +37,10 @@ public static void handle(Arguments argument, @Nullable String value) {
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");
}
}

Expand Down Expand Up @@ -111,4 +131,59 @@ private static void secret(String value) {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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 "";
}
}