99import xyz .gianlu .librespot .common .Utils ;
1010import xyz .gianlu .librespot .common .proto .Metadata ;
1111import xyz .gianlu .librespot .core .Session ;
12+ import xyz .gianlu .librespot .core .TokenProvider ;
1213import xyz .gianlu .librespot .mercury .MercuryClient ;
1314import xyz .gianlu .librespot .player .AbsChunckedInputStream ;
1415import xyz .gianlu .librespot .player .GeneralAudioStream ;
2627import java .sql .SQLException ;
2728import java .util .concurrent .ExecutorService ;
2829import java .util .concurrent .Executors ;
30+ import java .util .concurrent .atomic .AtomicReference ;
2931
3032import static xyz .gianlu .librespot .player .feeders .storage .ChannelManager .CHUNK_SIZE ;
3133
@@ -65,20 +67,20 @@ private InputStream getHead(@NotNull ByteString fileId) throws IOException {
6567 }
6668
6769 @ NotNull
68- public Streamer streamEpisode (@ NotNull Metadata .Episode episode , @ NotNull HttpUrl externalUrl , @ Nullable AbsChunckedInputStream .HaltListener haltListener ) throws IOException {
69- return new Streamer (new StreamId (episode ), SuperAudioFormat .MP3 , externalUrl , session .cache (), new NoopAudioDecrypt (), haltListener );
70+ public Streamer streamEpisode (@ NotNull Metadata .Episode episode , @ NotNull HttpUrl externalUrl , @ Nullable AbsChunckedInputStream .HaltListener haltListener ) throws IOException , CdnException {
71+ return new Streamer (new StreamId (episode ), SuperAudioFormat .MP3 , new CdnUrl ( externalUrl ) , session .cache (), new NoopAudioDecrypt (), haltListener );
7072 }
7173
7274 @ NotNull
7375 public Streamer streamTrack (@ NotNull Metadata .AudioFile file , @ NotNull byte [] key , @ Nullable AbsChunckedInputStream .HaltListener haltListener ) throws IOException , MercuryClient .MercuryException , CdnException {
7476 return new Streamer (new StreamId (file ), SuperAudioFormat .get (file .getFormat ()),
75- getAudioUrl (file .getFileId ()), session .cache (), new AesAudioDecrypt (key ), haltListener );
77+ getCdnUrl (file .getFileId ()), session .cache (), new AesAudioDecrypt (key ), haltListener );
7678 }
7779
7880 @ NotNull
79- private HttpUrl getAudioUrl (@ NotNull ByteString fileId ) throws IOException , MercuryClient .MercuryException , CdnException {
81+ private HttpUrl getAudioUrl (@ NotNull ByteString fileId , @ NotNull TokenProvider . ExpireListener listener ) throws IOException , CdnException , MercuryClient .MercuryException {
8082 try (Response resp = client .newCall (new Request .Builder ().get ()
81- .header ("Authorization" , "Bearer " + session .tokens ().get ("playlist-read" ))
83+ .header ("Authorization" , "Bearer " + session .tokens ().get ("playlist-read" , listener ))
8284 .url (String .format (STORAGE_RESOLVE_AUDIO_URL , Utils .bytesToHex (fileId )))
8385 .build ()).execute ()) {
8486
@@ -91,18 +93,31 @@ private HttpUrl getAudioUrl(@NotNull ByteString fileId) throws IOException, Merc
9193
9294 StorageResolve .StorageResolveResponse proto = StorageResolve .StorageResolveResponse .parseFrom (body .byteStream ());
9395 if (proto .getResult () == StorageResolve .StorageResolveResponse .Result .CDN ) {
94- return HttpUrl .get (proto .getCdnurl (session .random ().nextInt (proto .getCdnurlCount ())));
96+ String url = proto .getCdnurl (session .random ().nextInt (proto .getCdnurlCount ()));
97+ LOGGER .debug (String .format ("Fetched CDN url for %s: %s" , Utils .bytesToHex (fileId ), url ));
98+ return HttpUrl .get (url );
9599 } else {
96100 throw new CdnException (String .format ("Could not retrieve CDN url! {result: %s}" , proto .getResult ()));
97101 }
98102 }
99103 }
100104
105+ @ NotNull
106+ private CdnUrl getCdnUrl (@ NotNull ByteString fileId ) throws IOException , MercuryClient .MercuryException , CdnException {
107+ CdnUrl cdnUrl = new CdnUrl (fileId );
108+ cdnUrl .url = getAudioUrl (fileId , cdnUrl );
109+ return cdnUrl ;
110+ }
111+
101112 public static class CdnException extends Exception {
102113
103114 CdnException (@ NotNull String message ) {
104115 super (message );
105116 }
117+
118+ CdnException (Throwable ex ) {
119+ super (ex );
120+ }
106121 }
107122
108123 private static class InternalResponse {
@@ -115,12 +130,72 @@ private static class InternalResponse {
115130 }
116131 }
117132
133+ private class CdnUrl implements TokenProvider .ExpireListener {
134+ private final ByteString fileId ;
135+ private final AtomicReference <Exception > urlLock = new AtomicReference <>(null );
136+ private HttpUrl url ;
137+
138+ CdnUrl (@ NotNull HttpUrl url ) {
139+ this .url = url ;
140+ this .fileId = null ;
141+ }
142+
143+ CdnUrl (@ NotNull ByteString fileId ) {
144+ this .fileId = fileId ;
145+ this .url = null ;
146+ }
147+
148+ private void waitUrl () throws CdnException {
149+ if (url == null ) {
150+ synchronized (urlLock ) {
151+ try {
152+ urlLock .wait ();
153+ } catch (InterruptedException ex ) {
154+ throw new CdnException (ex );
155+ }
156+
157+ Exception ex = urlLock .get ();
158+ if (ex != null ) throw new CdnException (ex );
159+ }
160+ }
161+ }
162+
163+ @ Nullable
164+ String host () {
165+ return url == null ? null : url .host ();
166+ }
167+
168+ @ NotNull
169+ HttpUrl url () throws CdnException {
170+ waitUrl ();
171+ return url ;
172+ }
173+
174+ @ Override
175+ public void tokenExpired () {
176+ if (fileId == null ) throw new IllegalStateException ();
177+
178+ url = null ;
179+
180+ synchronized (urlLock ) {
181+ try {
182+ url = getAudioUrl (fileId , this );
183+ urlLock .set (null );
184+ } catch (IOException | CdnException | MercuryClient .MercuryException ex ) {
185+ urlLock .set (ex );
186+ }
187+
188+ urlLock .notifyAll ();
189+ }
190+ }
191+ }
192+
118193 public class Streamer implements GeneralAudioStream , GeneralWritableStream {
119194 private final StreamId streamId ;
120195 private final ExecutorService executorService = Executors .newCachedThreadPool ();
121196 private final SuperAudioFormat format ;
122197 private final AudioDecrypt audioDecrypt ;
123- private final HttpUrl cdnUrl ;
198+ private final CdnUrl cdnUrl ;
124199 private final int size ;
125200 private final byte [][] buffer ;
126201 private final boolean [] available ;
@@ -129,8 +204,8 @@ public class Streamer implements GeneralAudioStream, GeneralWritableStream {
129204 private final InternalStream internalStream ;
130205 private final CacheManager .Handler cacheHandler ;
131206
132- private Streamer (@ NotNull StreamId streamId , @ NotNull SuperAudioFormat format , @ NotNull HttpUrl cdnUrl , @ Nullable CacheManager cache ,
133- @ Nullable AudioDecrypt audioDecrypt , @ Nullable AbsChunckedInputStream .HaltListener haltListener ) throws IOException {
207+ private Streamer (@ NotNull StreamId streamId , @ NotNull SuperAudioFormat format , @ NotNull CdnUrl cdnUrl , @ Nullable CacheManager cache ,
208+ @ Nullable AudioDecrypt audioDecrypt , @ Nullable AbsChunckedInputStream .HaltListener haltListener ) throws IOException , CdnException {
134209 this .streamId = streamId ;
135210 this .format = format ;
136211 this .audioDecrypt = audioDecrypt ;
@@ -223,21 +298,21 @@ private void requestChunk(int index, boolean retried) {
223298 try {
224299 InternalResponse resp = request (index );
225300 writeChunk (resp .buffer , index , false );
226- } catch (IOException ex ) {
301+ } catch (IOException | CdnException ex ) {
227302 LOGGER .fatal (String .format ("Failed requesting chunk from network, index: %d, retried: %b" , index , retried ), ex );
228303 if (retried ) internalStream .notifyChunkError (index , new AbsChunckedInputStream .ChunkException (ex ));
229304 else requestChunk (index , true );
230305 }
231306 }
232307
233308 @ NotNull
234- public synchronized InternalResponse request (int chunk ) throws IOException {
309+ public synchronized InternalResponse request (int chunk ) throws IOException , CdnException {
235310 return request (CHUNK_SIZE * chunk , (chunk + 1 ) * CHUNK_SIZE - 1 );
236311 }
237312
238313 @ NotNull
239- public synchronized InternalResponse request (int rangeStart , int rangeEnd ) throws IOException {
240- try (Response resp = client .newCall (new Request .Builder ().get ().url (cdnUrl )
314+ public synchronized InternalResponse request (int rangeStart , int rangeEnd ) throws IOException , CdnException {
315+ try (Response resp = client .newCall (new Request .Builder ().get ().url (cdnUrl . url () )
241316 .header ("Range" , "bytes=" + rangeStart + "-" + rangeEnd )
242317 .build ()).execute ()) {
243318
0 commit comments