Skip to content

Commit de04915

Browse files
committed
Fixed #69
1 parent 98243cc commit de04915

7 files changed

Lines changed: 126 additions & 113 deletions

File tree

core/src/main/java/xyz/gianlu/librespot/cache/CacheManager.java

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package xyz.gianlu.librespot.cache;
22

3-
import com.google.protobuf.ByteString;
43
import org.apache.log4j.Logger;
54
import org.jetbrains.annotations.NotNull;
65
import org.jetbrains.annotations.Nullable;
76
import xyz.gianlu.librespot.common.Utils;
87
import xyz.gianlu.librespot.player.GeneralWritableStream;
8+
import xyz.gianlu.librespot.player.StreamId;
99

1010
import java.io.Closeable;
1111
import java.io.File;
@@ -31,7 +31,8 @@ public class CacheManager implements Closeable {
3131
private static final byte HEADER_TIMESTAMP = (byte) 0b11111111;
3232
private final boolean enabled;
3333
private final File parent;
34-
private final Map<ByteString, Handler> handlers = new ConcurrentHashMap<>();
34+
private final Map<String, Handler> fileHandlers = new ConcurrentHashMap<>();
35+
private final Map<String, Handler> episodeHandlers = new ConcurrentHashMap<>();
3536
private final Connection table;
3637

3738
public CacheManager(@NotNull Configuration conf) throws IOException {
@@ -59,11 +60,6 @@ public CacheManager(@NotNull Configuration conf) throws IOException {
5960
}
6061
}
6162

62-
@NotNull
63-
private static File getCacheFile(@NotNull File parent, @NotNull ByteString fileId) throws IOException {
64-
return getCacheFile(parent, Utils.bytesToHex(fileId));
65-
}
66-
6763
@NotNull
6864
private static File getCacheFile(@NotNull File parent, @NotNull String hex) throws IOException {
6965
String firstLevel = hex.substring(0, 2);
@@ -88,88 +84,104 @@ private void deleteCorruptedEntries() throws SQLException, IOException {
8884
if (!enabled) return;
8985

9086
List<String> toRemove = new ArrayList<>();
91-
try (PreparedStatement statement = table.prepareStatement("SELECT DISTINCT fileId FROM Headers")) {
87+
try (PreparedStatement statement = table.prepareStatement("SELECT DISTINCT streamId FROM Headers")) {
9288
ResultSet set = statement.executeQuery();
9389
while (set.next()) {
94-
String fileId = set.getString("fileId");
95-
if (!exists(parent, fileId))
96-
toRemove.add(fileId);
90+
String streamId = set.getString("streamId");
91+
if (!exists(parent, streamId))
92+
toRemove.add(streamId);
9793
}
9894
}
9995

100-
for (String fileId : toRemove)
101-
remove(fileId);
96+
for (String streamId : toRemove)
97+
remove(streamId);
10298
}
10399

104100
private void doCleanUp() throws SQLException, IOException {
105101
if (!enabled) return;
106102

107-
try (PreparedStatement statement = table.prepareStatement("SELECT fileId, value FROM Headers WHERE id=?")) {
103+
try (PreparedStatement statement = table.prepareStatement("SELECT streamId, value FROM Headers WHERE id=?")) {
108104
statement.setString(1, Utils.byteToHex(HEADER_TIMESTAMP));
109105

110106
ResultSet set = statement.executeQuery();
111107
while (set.next()) {
112108
long timestamp = Long.parseLong(set.getString("value"), 16) * 1000;
113109
if (System.currentTimeMillis() - timestamp > CLEAN_UP_THRESHOLD)
114-
remove(set.getString("fileId"));
110+
remove(set.getString("streamId"));
115111
}
116112
}
117113
}
118114

119-
private void remove(@NotNull String fileIdHex) throws SQLException, IOException {
115+
private void remove(@NotNull String streamId) throws SQLException, IOException {
120116
if (!enabled) return;
121117

122-
try (PreparedStatement statement = table.prepareStatement("DELETE FROM Headers WHERE fileId=?")) {
123-
statement.setString(1, fileIdHex);
118+
try (PreparedStatement statement = table.prepareStatement("DELETE FROM Headers WHERE streamId=?")) {
119+
statement.setString(1, streamId);
124120
statement.executeUpdate();
125121
}
126122

127-
try (PreparedStatement statement = table.prepareStatement("DELETE FROM Chunks WHERE fileId=?")) {
128-
statement.setString(1, fileIdHex);
123+
try (PreparedStatement statement = table.prepareStatement("DELETE FROM Chunks WHERE streamId=?")) {
124+
statement.setString(1, streamId);
129125
statement.executeUpdate();
130126
}
131127

132-
File file = getCacheFile(parent, fileIdHex);
128+
File file = getCacheFile(parent, streamId);
133129
if (file.exists() && !file.delete())
134130
LOGGER.warn("Couldn't delete cache file: " + file.getAbsolutePath());
135131

136-
LOGGER.trace(String.format("Removed %s from cache.", fileIdHex));
132+
LOGGER.trace(String.format("Removed %s from cache.", streamId));
137133
}
138134

139135
private void createTablesIfNeeded() throws SQLException {
140136
if (!enabled) return;
141137

142138
try (Statement statement = table.createStatement()) {
143-
statement.execute("CREATE TABLE IF NOT EXISTS Chunks ( `fileId` VARCHAR NOT NULL, `chunkIndex` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`fileId`,`chunkIndex`) )");
139+
statement.execute("CREATE TABLE IF NOT EXISTS Chunks ( `streamId` VARCHAR NOT NULL, `chunkIndex` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`streamId`,`chunkIndex`) )");
144140
}
145141

146142
try (Statement statement = table.createStatement()) {
147-
statement.execute("CREATE TABLE IF NOT EXISTS Headers ( `fileId` VARCHAR NOT NULL, `id` VARCHAR NOT NULL, `value` VARCHAR NOT NULL, PRIMARY KEY(`fileId`,`id`) )");
143+
statement.execute("CREATE TABLE IF NOT EXISTS Headers ( `streamId` VARCHAR NOT NULL, `id` VARCHAR NOT NULL, `value` VARCHAR NOT NULL, PRIMARY KEY(`streamId`,`id`) )");
148144
}
149145
}
150146

151147
@Override
152148
public void close() throws IOException {
153-
for (Handler handler : new ArrayList<>(handlers.values()))
149+
for (Handler handler : new ArrayList<>(fileHandlers.values()))
154150
handler.close();
155151
}
156152

157153
@Nullable
158-
public CacheManager.Handler forFileId(@NotNull ByteString fileId) throws IOException {
154+
public Handler forWhatever(@NotNull StreamId id) throws IOException {
155+
if (!enabled) return null;
156+
157+
if (id.isEpisode()) return forEpisode(id.getEpisodeGid());
158+
else return forFileId(id.getFileId());
159+
}
160+
161+
@Nullable
162+
public Handler forFileId(@NotNull String fileId) throws IOException {
159163
if (!enabled) return null;
160164

161-
Handler handler = handlers.get(fileId);
165+
Handler handler = fileHandlers.get(fileId);
162166
if (handler == null) {
163167
handler = new Handler(fileId, getCacheFile(parent, fileId));
164-
handlers.put(fileId, handler);
168+
fileHandlers.put(fileId, handler);
165169
}
166170

167171
return handler;
168172
}
169173

170174
@Nullable
171-
public CacheManager.Handler forFileId(@NotNull String fileId) throws IOException {
172-
return forFileId(ByteString.copyFrom(Utils.hexToBytes(fileId)));
175+
public Handler forEpisode(@NotNull String gid) throws IOException {
176+
if (!enabled) return null;
177+
178+
Handler handler = episodeHandlers.get(gid);
179+
if (handler == null) {
180+
handler = new Handler(gid, getCacheFile(parent, gid));
181+
episodeHandlers.put(gid, handler);
182+
}
183+
184+
return handler;
173185
}
174186

175187
public interface Configuration {
@@ -200,12 +212,12 @@ public static Header find(List<Header> headers, byte id) {
200212
}
201213

202214
public class Handler implements Closeable {
203-
private final ByteString fileId;
215+
private final String streamId;
204216
private final RandomAccessFile io;
205217
private boolean updatedTimestamp = false;
206218

207-
private Handler(@NotNull ByteString fileId, @NotNull File file) throws IOException {
208-
this.fileId = fileId;
219+
private Handler(@NotNull String streamId, @NotNull File file) throws IOException {
220+
this.streamId = streamId;
209221

210222
if (!file.exists() && !file.createNewFile())
211223
throw new IOException("Couldn't create cache file!");
@@ -216,21 +228,21 @@ private Handler(@NotNull ByteString fileId, @NotNull File file) throws IOExcepti
216228
private void updateTimestamp() {
217229
if (updatedTimestamp) return;
218230

219-
try (PreparedStatement statement = table.prepareStatement("MERGE INTO Headers (fileId, id, value) VALUES (?, ?, ?)")) {
220-
statement.setString(1, Utils.bytesToHex(fileId));
231+
try (PreparedStatement statement = table.prepareStatement("MERGE INTO Headers (streamId, id, value) VALUES (?, ?, ?)")) {
232+
statement.setString(1, streamId);
221233
statement.setString(2, Utils.byteToHex(HEADER_TIMESTAMP));
222234
statement.setString(3, Utils.bytesToHex(BigInteger.valueOf(System.currentTimeMillis() / 1000).toByteArray()));
223235

224236
statement.executeUpdate();
225237
updatedTimestamp = true;
226238
} catch (SQLException ex) {
227-
LOGGER.warn("Failed updating timestamp for " + Utils.bytesToHex(fileId), ex);
239+
LOGGER.warn("Failed updating timestamp for " + streamId, ex);
228240
}
229241
}
230242

231243
public void setHeader(byte id, byte[] value) throws SQLException {
232-
try (PreparedStatement statement = table.prepareStatement("MERGE INTO Headers (fileId, id, value) VALUES (?, ?, ?)")) {
233-
statement.setString(1, Utils.bytesToHex(fileId));
244+
try (PreparedStatement statement = table.prepareStatement("MERGE INTO Headers (streamId, id, value) VALUES (?, ?, ?)")) {
245+
statement.setString(1, streamId);
234246
statement.setString(2, Utils.byteToHex(id));
235247
statement.setString(3, Utils.bytesToHex(value));
236248

@@ -242,8 +254,8 @@ public void setHeader(byte id, byte[] value) throws SQLException {
242254

243255
@NotNull
244256
public List<Header> getAllHeaders() throws SQLException {
245-
try (PreparedStatement statement = table.prepareStatement("SELECT id, value FROM Headers WHERE fileId=?")) {
246-
statement.setString(1, Utils.bytesToHex(fileId));
257+
try (PreparedStatement statement = table.prepareStatement("SELECT id, value FROM Headers WHERE streamId=?")) {
258+
statement.setString(1, streamId);
247259

248260
List<Header> headers = new ArrayList<>();
249261
ResultSet set = statement.executeQuery();
@@ -256,8 +268,8 @@ public List<Header> getAllHeaders() throws SQLException {
256268

257269
@Nullable
258270
public byte[] getHeader(byte id) throws SQLException {
259-
try (PreparedStatement statement = table.prepareStatement("SELECT value FROM Headers WHERE fileId=? AND id=? LIMIT 1")) {
260-
statement.setString(1, Utils.bytesToHex(fileId));
271+
try (PreparedStatement statement = table.prepareStatement("SELECT value FROM Headers WHERE streamId=? AND id=? LIMIT 1")) {
272+
statement.setString(1, streamId);
261273
statement.setString(2, Utils.byteToHex(id));
262274

263275
ResultSet set = statement.executeQuery();
@@ -274,8 +286,8 @@ public boolean hasChunk(int index) throws SQLException, IOException {
274286
if (io.length() < (index + 1) * CHUNK_SIZE) return false;
275287
}
276288

277-
try (PreparedStatement statement = table.prepareStatement("SELECT available FROM Chunks WHERE fileId=? AND chunkIndex=? LIMIT 1")) {
278-
statement.setString(1, Utils.bytesToHex(fileId));
289+
try (PreparedStatement statement = table.prepareStatement("SELECT available FROM Chunks WHERE streamId=? AND chunkIndex=? LIMIT 1")) {
290+
statement.setString(1, streamId);
279291
statement.setInt(2, index);
280292

281293
ResultSet set = statement.executeQuery();
@@ -310,8 +322,8 @@ public void writeChunk(byte[] buffer, int index) throws IOException, SQLExceptio
310322
io.write(buffer);
311323
}
312324

313-
try (PreparedStatement statement = table.prepareStatement("MERGE INTO Chunks (fileId, chunkIndex, available) VALUES (?, ?, ?)")) {
314-
statement.setString(1, Utils.bytesToHex(fileId));
325+
try (PreparedStatement statement = table.prepareStatement("MERGE INTO Chunks (streamId, chunkIndex, available) VALUES (?, ?, ?)")) {
326+
statement.setString(1, streamId);
315327
statement.setInt(2, index);
316328
statement.setInt(3, 1);
317329

@@ -323,7 +335,7 @@ public void writeChunk(byte[] buffer, int index) throws IOException, SQLExceptio
323335

324336
@Override
325337
public void close() throws IOException {
326-
handlers.remove(fileId);
338+
fileHandlers.remove(streamId);
327339
synchronized (io) {
328340
io.close();
329341
}

core/src/main/java/xyz/gianlu/librespot/cdn/CdnManager.java

Lines changed: 17 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@
1010
import xyz.gianlu.librespot.common.proto.Metadata;
1111
import xyz.gianlu.librespot.core.Session;
1212
import xyz.gianlu.librespot.mercury.MercuryClient;
13-
import xyz.gianlu.librespot.player.AbsChunckedInputStream;
14-
import xyz.gianlu.librespot.player.AudioFileFetch;
15-
import xyz.gianlu.librespot.player.GeneralAudioStream;
16-
import xyz.gianlu.librespot.player.GeneralWritableStream;
13+
import xyz.gianlu.librespot.player.*;
1714
import xyz.gianlu.librespot.player.codecs.SuperAudioFormat;
1815
import xyz.gianlu.librespot.player.decrypt.AesAudioDecrypt;
1916
import xyz.gianlu.librespot.player.decrypt.AudioDecrypt;
@@ -22,8 +19,6 @@
2219
import java.io.IOException;
2320
import java.io.InputStream;
2421
import java.nio.ByteBuffer;
25-
import java.security.MessageDigest;
26-
import java.security.NoSuchAlgorithmException;
2722
import java.sql.SQLException;
2823
import java.util.concurrent.ExecutorService;
2924
import java.util.concurrent.Executors;
@@ -66,13 +61,14 @@ private InputStream getHead(@NotNull ByteString fileId) throws IOException {
6661
}
6762

6863
@NotNull
69-
public Streamer streamEpisode(@NotNull HttpUrl externalUrl) throws IOException {
70-
return new Streamer(AudioFileSurrogate.from(externalUrl), externalUrl, session.cache(), new NoopAudioDecrypt());
64+
public Streamer streamEpisode(@NotNull Metadata.Episode episode, @NotNull HttpUrl externalUrl) throws IOException {
65+
return new Streamer(new StreamId(episode), SuperAudioFormat.MP3, externalUrl, session.cache(), new NoopAudioDecrypt());
7166
}
7267

7368
@NotNull
7469
public Streamer streamTrack(@NotNull Metadata.AudioFile file, @NotNull byte[] key) throws IOException, MercuryClient.MercuryException, CdnException {
75-
return new Streamer(AudioFileSurrogate.from(file), getAudioUrl(file.getFileId()), session.cache(), new AesAudioDecrypt(key));
70+
return new Streamer(new StreamId(file), SuperAudioFormat.get(file.getFormat()),
71+
getAudioUrl(file.getFileId()), session.cache(), new AesAudioDecrypt(key));
7672
}
7773

7874
@NotNull
@@ -98,43 +94,6 @@ private HttpUrl getAudioUrl(@NotNull ByteString fileId) throws IOException, Merc
9894
}
9995
}
10096

101-
private static class AudioFileSurrogate {
102-
private final String fileId;
103-
private final SuperAudioFormat format;
104-
105-
AudioFileSurrogate(@NotNull String fileId, @NotNull SuperAudioFormat format) {
106-
this.fileId = fileId;
107-
this.format = format;
108-
}
109-
110-
@NotNull
111-
static AudioFileSurrogate from(@NotNull Metadata.AudioFile file) {
112-
return new AudioFileSurrogate(Utils.bytesToHex(file.getFileId()), SuperAudioFormat.get(file.getFormat()));
113-
}
114-
115-
@NotNull
116-
static AudioFileSurrogate from(@NotNull HttpUrl url) {
117-
// Generating fake file id!
118-
119-
try {
120-
byte[] hash = MessageDigest.getInstance("SHA1").digest(url.toString().getBytes());
121-
return new AudioFileSurrogate(Utils.bytesToHex(hash, 0, 20), SuperAudioFormat.MP3);
122-
} catch (NoSuchAlgorithmException ex) {
123-
throw new RuntimeException(ex);
124-
}
125-
}
126-
127-
@NotNull
128-
SuperAudioFormat getFormat() {
129-
return format;
130-
}
131-
132-
@NotNull
133-
String getFileId() {
134-
return fileId;
135-
}
136-
}
137-
13897
public static class CdnException extends Exception {
13998

14099
CdnException(@NotNull String message) {
@@ -153,8 +112,9 @@ private static class InternalResponse {
153112
}
154113

155114
public class Streamer implements GeneralAudioStream, GeneralWritableStream {
156-
private final AudioFileSurrogate file;
115+
private final StreamId streamId;
157116
private final ExecutorService executorService = Executors.newCachedThreadPool();
117+
private final SuperAudioFormat format;
158118
private final AudioDecrypt audioDecrypt;
159119
private final HttpUrl cdnUrl;
160120
private final int size;
@@ -165,11 +125,12 @@ public class Streamer implements GeneralAudioStream, GeneralWritableStream {
165125
private final InternalStream internalStream;
166126
private final CacheManager.Handler cacheHandler;
167127

168-
private Streamer(@NotNull AudioFileSurrogate file, @NotNull HttpUrl cdnUrl, @Nullable CacheManager cache, @Nullable AudioDecrypt audioDecrypt) throws IOException {
169-
this.file = file;
128+
private Streamer(@NotNull StreamId streamId, @NotNull SuperAudioFormat format, @NotNull HttpUrl cdnUrl, @Nullable CacheManager cache, @Nullable AudioDecrypt audioDecrypt) throws IOException {
129+
this.streamId = streamId;
130+
this.format = format;
170131
this.audioDecrypt = audioDecrypt;
171132
this.cdnUrl = cdnUrl;
172-
this.cacheHandler = cache != null ? cache.forFileId(file.getFileId()) : null;
133+
this.cacheHandler = cache != null ? cache.forWhatever(streamId) : null;
173134

174135
byte[] firstChunk;
175136
try {
@@ -220,7 +181,7 @@ public void writeChunk(@NotNull byte[] chunk, int chunkIndex, boolean cached) th
220181
}
221182
}
222183

223-
LOGGER.trace(String.format("Chunk %d/%d completed, cdn: %s, cached: %b, fileId: %s", chunkIndex, chunks, cdnUrl.host(), cached, file.getFileId()));
184+
LOGGER.trace(String.format("Chunk %d/%d completed, cdn: %s, cached: %b, stream: %s", chunkIndex, chunks, cdnUrl.host(), cached, describe()));
224185

225186
audioDecrypt.decryptChunk(chunkIndex, chunk, buffer[chunkIndex]);
226187
internalStream.notifyChunkAvailable(chunkIndex);
@@ -232,13 +193,14 @@ public void writeChunk(@NotNull byte[] chunk, int chunkIndex, boolean cached) th
232193
}
233194

234195
@Override
235-
public @NotNull String getFileIdHex() {
236-
return file.getFileId();
196+
public @NotNull SuperAudioFormat codec() {
197+
return format;
237198
}
238199

239200
@Override
240-
public @NotNull SuperAudioFormat codec() {
241-
return file.getFormat();
201+
public @NotNull String describe() {
202+
if (streamId.isEpisode()) return "{episodeGid: " + streamId.getEpisodeGid() + "}";
203+
else return "{fileId: " + streamId.getFileId() + "}";
242204
}
243205

244206
private void requestChunk(int index) {

0 commit comments

Comments
 (0)