Skip to content

Commit 6325ac0

Browse files
committed
Refactored TrackOrEpisode into MetadataWrapper + create LoadedStream from local file
1 parent 172c2d8 commit 6325ac0

13 files changed

Lines changed: 252 additions & 75 deletions

File tree

api/src/main/java/xyz/gianlu/librespot/api/handlers/EventsHandler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727
import org.jetbrains.annotations.Nullable;
2828
import org.jetbrains.annotations.Range;
2929
import xyz.gianlu.librespot.api.PlayerWrapper;
30+
import xyz.gianlu.librespot.audio.MetadataWrapper;
3031
import xyz.gianlu.librespot.common.ProtobufToJson;
3132
import xyz.gianlu.librespot.core.Session;
3233
import xyz.gianlu.librespot.metadata.PlayableId;
3334
import xyz.gianlu.librespot.player.Player;
34-
import xyz.gianlu.librespot.player.TrackOrEpisode;
3535

3636
public final class EventsHandler extends WebSocketProtocolHandshakeHandler implements Player.EventsListener, PlayerWrapper.Listener, Session.ReconnectionListener {
3737
private static final Logger LOGGER = LogManager.getLogger(EventsHandler.class);
@@ -54,7 +54,7 @@ public void onContextChanged(@NotNull Player player, @NotNull String newUri) {
5454
}
5555

5656
@Override
57-
public void onTrackChanged(@NotNull Player player, @NotNull PlayableId id, @Nullable TrackOrEpisode metadata) {
57+
public void onTrackChanged(@NotNull Player player, @NotNull PlayableId id, @Nullable MetadataWrapper metadata) {
5858
JsonObject obj = new JsonObject();
5959
obj.addProperty("event", "trackChanged");
6060
obj.addProperty("uri", id.toSpotifyUri());
@@ -98,7 +98,7 @@ public void onTrackSeeked(@NotNull Player player, long trackTime) {
9898
}
9999

100100
@Override
101-
public void onMetadataAvailable(@NotNull Player player, @NotNull TrackOrEpisode metadata) {
101+
public void onMetadataAvailable(@NotNull Player player, @NotNull MetadataWrapper metadata) {
102102
JsonObject obj = new JsonObject();
103103
obj.addProperty("event", "metadataAvailable");
104104
if (metadata.track != null) obj.add("track", ProtobufToJson.convert(metadata.track));

api/src/main/java/xyz/gianlu/librespot/api/handlers/PlayerHandler.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222
import org.jetbrains.annotations.Nullable;
2323
import xyz.gianlu.librespot.api.PlayerWrapper;
2424
import xyz.gianlu.librespot.api.Utils;
25+
import xyz.gianlu.librespot.audio.MetadataWrapper;
2526
import xyz.gianlu.librespot.common.ProtobufToJson;
2627
import xyz.gianlu.librespot.core.Session;
2728
import xyz.gianlu.librespot.metadata.EpisodeId;
2829
import xyz.gianlu.librespot.metadata.LocalId;
2930
import xyz.gianlu.librespot.metadata.PlayableId;
3031
import xyz.gianlu.librespot.metadata.TrackId;
3132
import xyz.gianlu.librespot.player.Player;
32-
import xyz.gianlu.librespot.player.TrackOrEpisode;
3333

3434
import java.util.Deque;
3535
import java.util.Map;
@@ -119,7 +119,7 @@ private static void current(HttpServerExchange exchange, @NotNull Player player)
119119
long time = player.time();
120120
obj.addProperty("trackTime", time);
121121

122-
TrackOrEpisode metadata = player.currentMetadata();
122+
MetadataWrapper metadata = player.currentMetadata();
123123
if (id instanceof TrackId) {
124124
if (metadata == null || metadata.track == null) {
125125
Utils.internalError(exchange, "Missing track metadata. Try again.");
@@ -135,7 +135,12 @@ private static void current(HttpServerExchange exchange, @NotNull Player player)
135135

136136
obj.add("episode", ProtobufToJson.convert(metadata.episode));
137137
} else if (id instanceof LocalId) {
138-
obj.addProperty("local", ((LocalId) id).fileName()); // TODO: Improve local files metadata
138+
JsonObject metadataObj = new JsonObject();
139+
metadataObj.addProperty("name", ((LocalId) id).fileName());
140+
metadataObj.addProperty("artist", ((LocalId) id).artist());
141+
metadataObj.addProperty("album", ((LocalId) id).album());
142+
metadataObj.addProperty("duration", ((LocalId) id).duration());
143+
obj.add("local", metadataObj);
139144
} else if (id != null) {
140145
Utils.internalError(exchange, "Invalid PlayableId: " + id);
141146
return;

player/src/main/java/xyz/gianlu/librespot/player/TrackOrEpisode.java renamed to lib/src/main/java/xyz/gianlu/librespot/audio/MetadataWrapper.java

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,36 @@
1414
* limitations under the License.
1515
*/
1616

17-
package xyz.gianlu.librespot.player;
17+
package xyz.gianlu.librespot.audio;
1818

1919
import com.spotify.metadata.Metadata;
2020
import org.jetbrains.annotations.Contract;
2121
import org.jetbrains.annotations.NotNull;
2222
import org.jetbrains.annotations.Nullable;
2323
import xyz.gianlu.librespot.common.Utils;
24+
import xyz.gianlu.librespot.metadata.LocalId;
2425
import xyz.gianlu.librespot.metadata.PlayableId;
2526

2627
/**
2728
* @author devgianlu
2829
*/
29-
public final class TrackOrEpisode {
30+
public final class MetadataWrapper {
3031
public final PlayableId id;
3132
public final Metadata.Track track;
3233
public final Metadata.Episode episode;
34+
private final LocalId localTrack;
3335

34-
@Contract("null, null -> fail")
35-
public TrackOrEpisode(@Nullable Metadata.Track track, @Nullable Metadata.Episode episode) {
36-
if (track == null && episode == null) throw new IllegalArgumentException();
36+
@Contract("null, null, null -> fail")
37+
public MetadataWrapper(@Nullable Metadata.Track track, @Nullable Metadata.Episode episode, @Nullable LocalId localTrack) {
38+
if (track == null && episode == null && localTrack == null) throw new IllegalArgumentException();
3739

3840
this.track = track;
3941
this.episode = episode;
42+
this.localTrack = localTrack;
4043

4144
if (track != null) id = PlayableId.from(track);
42-
else id = PlayableId.from(episode);
45+
else if (episode != null) id = PlayableId.from(episode);
46+
else id = localTrack;
4347
}
4448

4549
public boolean isTrack() {
@@ -50,11 +54,17 @@ public boolean isEpisode() {
5054
return episode != null;
5155
}
5256

57+
public boolean isLocalTrack() {
58+
return localTrack != null;
59+
}
60+
5361
/**
5462
* @return The track/episode duration
5563
*/
5664
public int duration() {
57-
return track != null ? track.getDuration() : episode.getDuration();
65+
if (track != null) return track.getDuration();
66+
else if (episode != null) return episode.getDuration();
67+
else return localTrack.duration();
5868
}
5969

6070
/**
@@ -65,9 +75,11 @@ public Metadata.ImageGroup getCoverImage() {
6575
if (track != null) {
6676
if (track.hasAlbum() && track.getAlbum().hasCoverGroup())
6777
return track.getAlbum().getCoverGroup();
68-
} else {
78+
} else if (episode != null) {
6979
if (episode.hasCoverImage())
7080
return episode.getCoverImage();
81+
} else {
82+
// TODO: Fetch album image from track file
7183
}
7284

7385
return null;
@@ -78,22 +90,28 @@ public Metadata.ImageGroup getCoverImage() {
7890
*/
7991
@NotNull
8092
public String getName() {
81-
return track != null ? track.getName() : episode.getName();
93+
if (track != null) return track.getName();
94+
else if (episode != null) return episode.getName();
95+
else return localTrack.fileName();
8296
}
8397

8498
/**
8599
* @return The track album name or episode show name
86100
*/
87101
@NotNull
88102
public String getAlbumName() {
89-
return track != null ? track.getAlbum().getName() : episode.getShow().getName();
103+
if (track != null) return track.getAlbum().getName();
104+
else if (episode != null) return episode.getShow().getName();
105+
else return localTrack.album();
90106
}
91107

92108
/**
93109
* @return The track artists or show publisher
94110
*/
95111
@NotNull
96112
public String getArtist() {
97-
return track != null ? Utils.artistsToString(track.getArtistList()) : episode.getShow().getPublisher();
113+
if (track != null) return Utils.artistsToString(track.getArtistList());
114+
else if (episode != null) return episode.getShow().getPublisher();
115+
else return localTrack.artist();
98116
}
99117
}

lib/src/main/java/xyz/gianlu/librespot/audio/PlayableContentFeeder.java

Lines changed: 114 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030
import xyz.gianlu.librespot.audio.cdn.CdnFeedHelper;
3131
import xyz.gianlu.librespot.audio.cdn.CdnManager;
3232
import xyz.gianlu.librespot.audio.format.AudioQualityPicker;
33+
import xyz.gianlu.librespot.audio.format.SuperAudioFormat;
3334
import xyz.gianlu.librespot.audio.storage.AudioFileFetch;
3435
import xyz.gianlu.librespot.audio.storage.StorageFeedHelper;
36+
import xyz.gianlu.librespot.common.NameThreadFactory;
3537
import xyz.gianlu.librespot.common.Utils;
3638
import xyz.gianlu.librespot.core.Session;
3739
import xyz.gianlu.librespot.mercury.MercuryClient;
@@ -40,8 +42,14 @@
4042
import xyz.gianlu.librespot.metadata.PlayableId;
4143
import xyz.gianlu.librespot.metadata.TrackId;
4244

45+
import java.io.File;
4346
import java.io.IOException;
47+
import java.io.RandomAccessFile;
4448
import java.util.List;
49+
import java.util.concurrent.ExecutorService;
50+
import java.util.concurrent.Executors;
51+
52+
import static xyz.gianlu.librespot.audio.storage.ChannelManager.CHUNK_SIZE;
4553

4654
/**
4755
* @author Gianlu
@@ -78,8 +86,6 @@ public final LoadedStream load(@NotNull PlayableId id, @NotNull AudioQualityPick
7886
return loadTrack((TrackId) id, audioQualityPicker, preload, haltListener);
7987
else if (id instanceof EpisodeId)
8088
return loadEpisode((EpisodeId) id, audioQualityPicker, preload, haltListener);
81-
else if (id instanceof LocalId)
82-
throw new UnsupportedOperationException("Cannot play local files!"); // TODO: Play this
8389
else
8490
throw new IllegalArgumentException("Unknown content: " + id);
8591
}
@@ -177,27 +183,127 @@ private LoadedStream loadEpisode(@NotNull EpisodeId id, @NotNull AudioQualityPic
177183
}
178184
}
179185

186+
private static class FileAudioStream implements GeneralAudioStream {
187+
private static final Logger LOGGER = LoggerFactory.getLogger(FileAudioStream.class);
188+
private final File file;
189+
private final RandomAccessFile raf;
190+
private final byte[][] buffer;
191+
private final int chunks;
192+
private final int size;
193+
private final boolean[] available;
194+
private final boolean[] requested;
195+
private final ExecutorService executorService = Executors.newCachedThreadPool(new NameThreadFactory((r) -> "file-async-" + r.hashCode()));
196+
197+
FileAudioStream(File file) throws IOException {
198+
this.file = file;
199+
this.raf = new RandomAccessFile(file, "r");
200+
201+
this.size = (int) raf.length();
202+
this.chunks = (size + CHUNK_SIZE - 1) / CHUNK_SIZE;
203+
this.buffer = new byte[chunks][];
204+
this.available = new boolean[chunks];
205+
this.requested = new boolean[chunks];
206+
}
207+
208+
@Override
209+
public @NotNull AbsChunkedInputStream stream() {
210+
return new AbsChunkedInputStream(false) {
211+
@Override
212+
protected byte[][] buffer() {
213+
return buffer;
214+
}
215+
216+
@Override
217+
public int size() {
218+
return size;
219+
}
220+
221+
@Override
222+
protected boolean[] requestedChunks() {
223+
return requested;
224+
}
225+
226+
@Override
227+
protected boolean[] availableChunks() {
228+
return available;
229+
}
230+
231+
@Override
232+
protected int chunks() {
233+
return chunks;
234+
}
235+
236+
@Override
237+
protected void requestChunkFromStream(int index) {
238+
executorService.submit(() -> {
239+
try {
240+
raf.seek((long) index * CHUNK_SIZE);
241+
raf.read(buffer[index]);
242+
notifyChunkAvailable(index);
243+
} catch (IOException ex) {
244+
notifyChunkError(index, new ChunkException(ex));
245+
}
246+
});
247+
}
248+
249+
@Override
250+
public void streamReadHalted(int chunk, long time) {
251+
LOGGER.warn("Not dispatching stream read halted event {chunk: {}}", chunk);
252+
}
253+
254+
@Override
255+
public void streamReadResumed(int chunk, long time) {
256+
LOGGER.warn("Not dispatching stream read resumed event {chunk: {}}", chunk);
257+
}
258+
};
259+
}
260+
261+
@Override
262+
public @NotNull SuperAudioFormat codec() {
263+
return SuperAudioFormat.MP3; // FIXME: Detect codec
264+
}
265+
266+
@Override
267+
public @NotNull String describe() {
268+
return "{file: " + file.getAbsolutePath() + "}";
269+
}
270+
271+
@Override
272+
public int decryptTimeMs() {
273+
return 0;
274+
}
275+
}
276+
180277
public static class LoadedStream {
181-
public final Metadata.Episode episode;
182-
public final Metadata.Track track;
278+
public final MetadataWrapper metadata;
183279
public final GeneralAudioStream in;
184280
public final NormalizationData normalizationData;
185281
public final Metrics metrics;
186282

187283
public LoadedStream(@NotNull Metadata.Track track, @NotNull GeneralAudioStream in, @Nullable NormalizationData normalizationData, @NotNull Metrics metrics) {
188-
this.track = track;
284+
this.metadata = new MetadataWrapper(track, null, null);
189285
this.in = in;
190286
this.normalizationData = normalizationData;
191287
this.metrics = metrics;
192-
this.episode = null;
193288
}
194289

195290
public LoadedStream(@NotNull Metadata.Episode episode, @NotNull GeneralAudioStream in, @Nullable NormalizationData normalizationData, @NotNull Metrics metrics) {
196-
this.episode = episode;
291+
this.metadata = new MetadataWrapper(null, episode, null);
197292
this.in = in;
198293
this.normalizationData = normalizationData;
199294
this.metrics = metrics;
200-
this.track = null;
295+
}
296+
297+
private LoadedStream(@NotNull LocalId id, @NotNull GeneralAudioStream in) {
298+
this.metadata = new MetadataWrapper(null, null, id);
299+
this.in = in;
300+
this.normalizationData = null;
301+
this.metrics = new Metrics(null, false, 0);
302+
}
303+
304+
@NotNull
305+
public static LoadedStream forLocalFile(@NotNull LocalId id, @NotNull File file) throws IOException {
306+
return new LoadedStream(id, new FileAudioStream(file));
201307
}
202308
}
203309

lib/src/main/java/xyz/gianlu/librespot/audio/cdn/CdnManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ private Streamer(@NotNull StreamId streamId, @NotNull SuperAudioFormat format, @
244244

245245
String[] split = Utils.split(contentRange, '/');
246246
size = Integer.parseInt(split[1]);
247-
chunks = (int) Math.ceil((float) size / (float) CHUNK_SIZE);
247+
chunks = (size + CHUNK_SIZE - 1) / CHUNK_SIZE;
248248

249249
if (cacheHandler != null)
250250
cacheHandler.setHeader(AudioFileFetch.HEADER_SIZE, ByteBuffer.allocate(4).putInt(size / 4).array());

0 commit comments

Comments
 (0)