55import com .spotify .context .ContextTrackOuterClass .ContextTrack ;
66import com .spotify .metadata .Metadata ;
77import com .spotify .transfer .TransferStateOuterClass ;
8+ import okhttp3 .Request ;
9+ import okhttp3 .Response ;
10+ import okhttp3 .ResponseBody ;
811import org .apache .log4j .Logger ;
912import org .jetbrains .annotations .NotNull ;
1013import org .jetbrains .annotations .Nullable ;
1619import xyz .gianlu .librespot .core .Session ;
1720import xyz .gianlu .librespot .mercury .MercuryClient ;
1821import xyz .gianlu .librespot .mercury .MercuryRequests ;
22+ import xyz .gianlu .librespot .mercury .model .ImageId ;
1923import xyz .gianlu .librespot .mercury .model .PlayableId ;
2024import xyz .gianlu .librespot .player .PlayerRunner .PushToMixerReason ;
2125import 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
0 commit comments