Skip to content

Commit 53f8c17

Browse files
uvjustindevgianlu
andauthored
Add progress and picture metadata (#182)
* Add progress and picture metadata * Some refactoring * Fixed build * Another try at fixing the build Co-authored-by: Gianlu <[email protected]>
1 parent 14233f5 commit 53f8c17

4 files changed

Lines changed: 94 additions & 20 deletions

File tree

.travis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ language: java
22
os: linux
33

44
before_install:
5-
- echo $GPG_SECRET_KEYS | base64 --decode | $GPG_EXECUTABLE --import
6-
- echo $GPG_OWNERTRUST | base64 --decode | $GPG_EXECUTABLE --import-ownertrust
5+
- [ "${TRAVIS_PULL_REQUEST}" = "false" ] && echo $GPG_SECRET_KEYS | base64 --decode | $GPG_EXECUTABLE --import
6+
- [ "${TRAVIS_PULL_REQUEST}" = "false" ] && echo $GPG_OWNERTRUST | base64 --decode | $GPG_EXECUTABLE --import-ownertrust
77

88
script:
99
- mvn clean test -Pdebug -B -U -Dgpg.skip -Dmaven.javadoc.skip=true
1010

1111
after_script:
12-
- test $TRAVIS_PULL_REQUEST = "false" && test $TRAVIS_BRANCH = "dev" && mvn deploy --settings .maven.xml -B -U -Prelease
12+
- [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${$TRAVIS_BRANCH}" = "dev" ] && mvn deploy --settings .maven.xml -B -U -Prelease
1313

1414
before_deploy:
1515
- mvn help:evaluate -N -Dexpression=project.version|grep -v '\['

core/src/main/java/xyz/gianlu/librespot/mercury/model/ImageId.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* @author Gianlu
1313
*/
1414
public final class ImageId implements SpotifyId {
15+
public static final String[] IMAGE_SIZES_URLS = new String[]{"image_xlarge_url", "image_large_url", "image_url", "image_small_url"};
1516
private static final Pattern PATTERN = Pattern.compile("spotify:image:(.{40})");
1617
private final String hexId;
1718

@@ -59,4 +60,9 @@ public static void putAsMetadata(@NotNull Player.ProvidedTrack.Builder builder,
5960
public @NotNull String toSpotifyUri() {
6061
return "spotify:image:" + hexId;
6162
}
63+
64+
@NotNull
65+
public String hexId() {
66+
return hexId;
67+
}
6268
}

core/src/main/java/xyz/gianlu/librespot/player/Player.java

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import com.spotify.context.ContextTrackOuterClass.ContextTrack;
66
import com.spotify.metadata.Metadata;
77
import com.spotify.transfer.TransferStateOuterClass;
8+
import okhttp3.Request;
9+
import okhttp3.Response;
10+
import okhttp3.ResponseBody;
811
import org.apache.log4j.Logger;
912
import org.jetbrains.annotations.NotNull;
1013
import org.jetbrains.annotations.Nullable;
@@ -16,6 +19,7 @@
1619
import xyz.gianlu.librespot.core.Session;
1720
import xyz.gianlu.librespot.mercury.MercuryClient;
1821
import xyz.gianlu.librespot.mercury.MercuryRequests;
22+
import xyz.gianlu.librespot.mercury.model.ImageId;
1923
import xyz.gianlu.librespot.mercury.model.PlayableId;
2024
import xyz.gianlu.librespot.player.PlayerRunner.PushToMixerReason;
2125
import xyz.gianlu.librespot.player.PlayerRunner.TrackHandler;
@@ -730,6 +734,8 @@ private static class MetadataPipe {
730734
private static final String CODE_ASAL = "6173616c";
731735
private static final String CODE_MINM = "6d696e6d";
732736
private static final String CODE_PVOL = "70766f6c";
737+
private static final String CODE_PRGR = "70726772";
738+
private static final String CODE_PICT = "50494354";
733739
private final FileOutputStream out;
734740

735741
MetadataPipe(@NotNull Configuration conf) throws FileNotFoundException {
@@ -738,25 +744,33 @@ private static class MetadataPipe {
738744
else out = null;
739745
}
740746

741-
private void send(@NotNull String type, @NotNull String code, @Nullable String payload) throws IOException {
742-
if (out == null) return;
743-
744-
if (payload != null) {
745-
out.write(String.format("<item><type>%s</type><code>%s</code><length>%d</length>\n<data encoding=\"base64\">\n%s</data></item>", type, code,
746-
payload.length(), Base64.getEncoder().encodeToString(payload.getBytes(StandardCharsets.UTF_8))).getBytes(StandardCharsets.UTF_8));
747-
} else {
748-
out.write(String.format("<item><type>%s</type><code>%s</code><length>0</length></item>", type, code).getBytes(StandardCharsets.UTF_8));
747+
void safeSend(@NotNull String type, @NotNull String code, @Nullable String payload) {
748+
try {
749+
send(type, code, payload == null ? null : payload.getBytes(StandardCharsets.UTF_8));
750+
} catch (IOException ex) {
751+
LOGGER.error("Failed sending metadata through pipe!", ex);
749752
}
750753
}
751754

752-
void safeSend(@NotNull String type, @NotNull String code, @Nullable String payload) {
755+
void safeSend(@NotNull String type, @NotNull String code, @Nullable byte[] payload) {
753756
try {
754757
send(type, code, payload);
755758
} catch (IOException ex) {
756759
LOGGER.error("Failed sending metadata through pipe!", ex);
757760
}
758761
}
759762

763+
private void send(@NotNull String type, @NotNull String code, @Nullable byte[] payload) throws IOException {
764+
if (out == null) return;
765+
766+
if (payload != null) {
767+
out.write(String.format("<item><type>%s</type><code>%s</code><length>%d</length>\n<data encoding=\"base64\">%s</data></item>\n", type, code,
768+
payload.length, Base64.getEncoder().encodeToString(payload)).getBytes(StandardCharsets.UTF_8));
769+
} else {
770+
out.write(String.format("<item><type>%s</type><code>%s</code><length>0</length></item>\n", type, code).getBytes(StandardCharsets.UTF_8));
771+
}
772+
}
773+
760774
boolean enabled() {
761775
return out != null;
762776
}
@@ -775,6 +789,58 @@ private class EventsDispatcher {
775789
}
776790
}
777791

792+
private void sendImage() {
793+
PlayableId id = state.getCurrentPlayable();
794+
if (id == null) return;
795+
796+
Map<String, String> metadata = state.metadataFor(id);
797+
ImageId image = null;
798+
for (String key : ImageId.IMAGE_SIZES_URLS) {
799+
if (metadata.containsKey(key)) {
800+
image = ImageId.fromUri(metadata.get(key));
801+
break;
802+
}
803+
}
804+
805+
if (image == null) {
806+
LOGGER.warn("No image found in metadata: " + id);
807+
return;
808+
}
809+
810+
try (Response resp = session.client().newCall(new Request.Builder()
811+
.url("http://open.spotify.com/image/" + image.hexId()).build())
812+
.execute()) {
813+
ResponseBody body;
814+
if (resp.code() == 200 && (body = resp.body()) != null)
815+
metadataPipe.safeSend(MetadataPipe.TYPE_SSNC, MetadataPipe.CODE_PICT, body.bytes());
816+
else
817+
LOGGER.warn(String.format("Failed download image. {id: %s, code: %d}", image.hexId(), resp.code()));
818+
} catch (IOException ex) {
819+
LOGGER.warn("Failed download image.", ex);
820+
}
821+
}
822+
823+
private void sendProgress() {
824+
PlayableId id = state.getCurrentPlayable();
825+
if (id == null || trackHandler == null || !trackHandler.isPlayable(id)) return;
826+
827+
Metadata.Track track;
828+
Metadata.Episode episode;
829+
int duration = -1;
830+
if ((track = trackHandler.track()) != null) {
831+
if (track.hasDuration()) duration = track.getDuration();
832+
} else if ((episode = trackHandler.episode()) != null) {
833+
if (episode.hasDuration()) duration = episode.getDuration();
834+
}
835+
836+
if (duration == -1)
837+
return;
838+
839+
String data = String.format("1/%.0f/%.0f", state.getPosition() * PlayerRunner.OUTPUT_FORMAT.getSampleRate() / 1000 + 1,
840+
duration * PlayerRunner.OUTPUT_FORMAT.getSampleRate() / 1000 + 1);
841+
metadataPipe.safeSend(MetadataPipe.TYPE_SSNC, MetadataPipe.CODE_PRGR, data);
842+
}
843+
778844
void playbackPaused() {
779845
long trackTime = state.getPosition();
780846
for (EventsListener l : new ArrayList<>(listeners))
@@ -816,6 +882,8 @@ void trackChanged() {
816882
void seeked(int pos) {
817883
for (EventsListener l : new ArrayList<>(listeners))
818884
executorService.execute(() -> l.onTrackSeeked(pos));
885+
886+
if (metadataPipe.enabled()) sendProgress();
819887
}
820888

821889
void volumeChanged(@Range(from = 0, to = PlayerRunner.VOLUME_MAX) int value) {
@@ -827,10 +895,7 @@ void volumeChanged(@Range(from = 0, to = PlayerRunner.VOLUME_MAX) int value) {
827895
if (metadataPipe.enabled()) {
828896
float xmlValue;
829897
if (value == 0) xmlValue = 144.0f;
830-
else if (value == 1) xmlValue = -30.0f;
831-
else if (value == 0xFFFF) xmlValue = 0.0f;
832-
else xmlValue = (value - 0xFFFF) * 30.0f / 0xFFFE;
833-
898+
else xmlValue = (value - PlayerRunner.VOLUME_MAX) * 30.0f / (PlayerRunner.VOLUME_MAX - 1);
834899
String volData = String.format("%.2f,0.00,0.00,0.00", xmlValue);
835900
metadataPipe.safeSend(MetadataPipe.TYPE_SSNC, MetadataPipe.CODE_PVOL, volData);
836901
}
@@ -855,6 +920,9 @@ void metadataAvailable() {
855920

856921
String artist = track != null ? Utils.artistsToString(track.getArtistList()) : episode.getShow().getPublisher();
857922
metadataPipe.safeSend(MetadataPipe.TYPE_CORE, MetadataPipe.CODE_ASAR, artist);
923+
924+
sendProgress();
925+
sendImage();
858926
}
859927
}
860928

core/src/main/java/xyz/gianlu/librespot/player/PlayerRunner.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class PlayerRunner implements Runnable, Closeable {
4040
public static final int VOLUME_STEPS = 64;
4141
public static final int VOLUME_MAX = 65536;
4242
public static final int VOLUME_ONE_STEP = VOLUME_MAX / VOLUME_STEPS;
43+
public static final AudioFormat OUTPUT_FORMAT = new AudioFormat(44100, 16, 2, true, false);
4344
private static final Logger LOGGER = Logger.getLogger(PlayerRunner.class);
4445
private static final AtomicInteger IDS = new AtomicInteger(0);
4546
private final Session session;
@@ -50,7 +51,7 @@ public class PlayerRunner implements Runnable, Closeable {
5051
private final BlockingQueue<CommandBundle> commands = new LinkedBlockingQueue<>();
5152
private final Object pauseLock = new Object();
5253
private final Output output;
53-
private final MixingLine mixing = new MixingLine(Output.OUTPUT_FORMAT);
54+
private final MixingLine mixing = new MixingLine(OUTPUT_FORMAT);
5455
private volatile boolean closed = false;
5556
private volatile boolean paused = true;
5657
private TrackHandler firstHandler = null;
@@ -261,7 +262,6 @@ public interface Listener {
261262
}
262263

263264
private static class Output implements Closeable {
264-
private static final AudioFormat OUTPUT_FORMAT = new AudioFormat(44100, 16, 2, true, false);
265265
private final File pipe;
266266
private final MixingLine mixing;
267267
private final Player.Configuration conf;
@@ -299,8 +299,8 @@ private static float calcLogarithmic(int val) {
299299
private void acquireLine() throws LineUnavailableException {
300300
if (line != null) return;
301301

302-
line = LineHelper.getLineFor(conf, Output.OUTPUT_FORMAT);
303-
line.open(Output.OUTPUT_FORMAT);
302+
line = LineHelper.getLineFor(conf, OUTPUT_FORMAT);
303+
line.open(OUTPUT_FORMAT);
304304

305305
if (lastVolume != -1) setVolume(lastVolume);
306306
}

0 commit comments

Comments
 (0)