immediately concatenate bytes without creating files
This commit is contained in:
@@ -4,7 +4,7 @@ import com.vk.core.concurrent.VkExecutors;
|
||||
import io.reactivex.Scheduler;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
|
||||
public class VTExecutors {
|
||||
public static ExecutorService getMusicDownloadExecutor() {
|
||||
@@ -32,7 +32,20 @@ public class VTExecutors {
|
||||
}
|
||||
|
||||
private static class LazyHolder {
|
||||
public static final ExecutorService INSTANCE = Executors.newSingleThreadExecutor();
|
||||
|
||||
private static final int PARALLELISM = clamp(
|
||||
(Runtime.getRuntime().availableProcessors() >> 1) - 1,
|
||||
1, 7);
|
||||
|
||||
private static final ExecutorService INSTANCE =
|
||||
new ForkJoinPool(PARALLELISM,
|
||||
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
|
||||
(t, e) -> {},
|
||||
/* FIFO */ true);
|
||||
|
||||
private static int clamp(int value, int min, int max) {
|
||||
return Math.min(max, Math.max(value, min));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package ru.vtosters.lite.music.converter.ts;
|
||||
|
||||
import android.os.Build;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
public class TSMerger {
|
||||
/**
|
||||
* Merge multiple .ts files into one .ts file
|
||||
*
|
||||
* @param in Directory with .ts files
|
||||
*/
|
||||
public static void merge(File in, File out) {
|
||||
File[] tsesList = in.listFiles();
|
||||
|
||||
if (tsesList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream mergedByteArrayStream = new ByteArrayOutputStream();
|
||||
|
||||
sortTsesList(tsesList);
|
||||
|
||||
for (File file : tsesList) {
|
||||
if (file.getName().endsWith(".ts")) {
|
||||
try {
|
||||
mergedByteArrayStream.write(IOUtils.readFully(file));
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeOutputFile(out, mergedByteArrayStream);
|
||||
}
|
||||
|
||||
private static void sortTsesList(File[] tsesList) {
|
||||
Arrays.sort(tsesList, Comparator.comparingInt(TSMerger::getFileIndex));
|
||||
}
|
||||
|
||||
private static int getFileIndex(File file) {
|
||||
String fileName = file.getName();
|
||||
int dotIndex = fileName.indexOf('.', 1);
|
||||
return Integer.parseInt(fileName.substring(0, dotIndex));
|
||||
}
|
||||
|
||||
private static void writeOutputFile(File out, ByteArrayOutputStream mergedByteArrayStream) {
|
||||
byte[] bytes = mergedByteArrayStream.toByteArray();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
Files.write(out.toPath(), bytes);
|
||||
} catch (IOException ignored) {
|
||||
// ignored
|
||||
}
|
||||
} else {
|
||||
try (FileOutputStream fos = new FileOutputStream(out)) {
|
||||
fos.write(bytes);
|
||||
} catch (IOException ignored) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,28 +7,43 @@ import com.google.android.exoplayer2.source.hls.playlist.e;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.f;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.f.a;
|
||||
import com.vk.dto.music.MusicTrack;
|
||||
import java8.util.concurrent.CompletableFuture;
|
||||
import java8.util.concurrent.CompletionException;
|
||||
|
||||
|
||||
import ru.vtosters.lite.music.cache.MusicCacheImpl;
|
||||
import ru.vtosters.lite.music.converter.ts.FFMpeg;
|
||||
import ru.vtosters.lite.music.converter.ts.TSMerger;
|
||||
import ru.vtosters.lite.music.interfaces.Callback;
|
||||
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
||||
import ru.vtosters.lite.utils.AndroidUtils;
|
||||
import ru.vtosters.lite.utils.IOUtils;
|
||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class M3UDownloader
|
||||
implements ITrackDownloader {
|
||||
|
||||
private static final Cipher CIPHER;
|
||||
|
||||
static {
|
||||
try {
|
||||
CIPHER = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static M3UDownloader getInstance() {
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
@@ -47,94 +62,77 @@ public class M3UDownloader
|
||||
var resultTs = getResultTsFile(track);
|
||||
var resultMp3 = getResultMp3File(outDir, cache, track);
|
||||
try {
|
||||
CompletableFuture.allOf(createFutures(baseUri, segments, progress, callback, track))
|
||||
.thenRun(() -> {
|
||||
if (cache) {
|
||||
try {
|
||||
ThumbnailDownloader.downloadThumbnails(track);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to download thumbs", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.thenRun(() -> {
|
||||
try {
|
||||
TSMerger.merge(getTsesDir(track), resultTs);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Failed to merge ts files", e);
|
||||
}
|
||||
})
|
||||
.thenRun(() -> {
|
||||
try {
|
||||
FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("FFmpeg error", e);
|
||||
}
|
||||
})
|
||||
.thenRun(() -> callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / segments.size())))
|
||||
.thenRun(() -> {
|
||||
if (cache) {
|
||||
MusicCacheImpl.addTrack(track);
|
||||
}
|
||||
})
|
||||
.whenComplete((unused, tr) -> {
|
||||
if (tr != null) {
|
||||
IOUtils.deleteRecursive(MusicCacheStorageUtils.getTrackDirById(track.y1()));
|
||||
callback.onFailure();
|
||||
} else {
|
||||
callback.onSuccess();
|
||||
}
|
||||
IOUtils.deleteRecursive(getTsesDir(track));
|
||||
})
|
||||
.join();
|
||||
} catch (CompletionException e) {
|
||||
|
||||
byte[] buff = downloadTs(baseUri, segments, progress, callback);
|
||||
|
||||
IOUtils.writeToFile(resultTs, buff);
|
||||
|
||||
if (cache) {
|
||||
try {
|
||||
ThumbnailDownloader.downloadThumbnails(track);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to download thumbs", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("FFmpeg error", e);
|
||||
}
|
||||
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / segments.size()));
|
||||
if (cache) {
|
||||
try {
|
||||
MusicCacheImpl.addTrack(track);
|
||||
callback.onSuccess();
|
||||
} catch (Exception e) {
|
||||
callback.onFailure();
|
||||
}
|
||||
}
|
||||
} catch (InvalidAlgorithmParameterException |
|
||||
InvalidKeyException |
|
||||
IllegalBlockSizeException |
|
||||
BadPaddingException e) {
|
||||
Log.e("M3UDownloader", "Failed to download track", e.getCause());
|
||||
} finally {
|
||||
resultTs.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
static CompletableFuture[] createFutures(
|
||||
String baseUri,
|
||||
List<a> segments,
|
||||
AtomicInteger progress,
|
||||
Callback callback,
|
||||
MusicTrack track) {
|
||||
var futures = new CompletableFuture[segments.size()];
|
||||
for (var i = 0; i < segments.size(); ++i) {
|
||||
var segment = segments.get(i);
|
||||
var filename = i + ".ts";
|
||||
futures[i] = CompletableFuture.runAsync(() -> processSegment(baseUri, segment, filename, progress, segments.size(), callback, track));
|
||||
}
|
||||
return futures;
|
||||
}
|
||||
private static byte[]
|
||||
downloadTs(String baseUri, List<a> segments,
|
||||
AtomicInteger progress, Callback callback
|
||||
) throws IOException, InvalidAlgorithmParameterException,
|
||||
InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
|
||||
byte[] total = new byte[0];
|
||||
|
||||
private static void processSegment(
|
||||
String baseUri,
|
||||
a segment,
|
||||
String filename,
|
||||
AtomicInteger progress,
|
||||
int size,
|
||||
Callback callback,
|
||||
MusicTrack track) {
|
||||
try {
|
||||
int size = segments.size();
|
||||
for (var segment : segments) {
|
||||
var buff = IOUtils.readFully(IOUtils.openStream(baseUri + segment.a/*url*/));
|
||||
if (!TextUtils.isEmpty(segment.g)) {
|
||||
var cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
||||
var cipherKey = new SecretKeySpec(IOUtils.readFully(IOUtils.openStream(segment.g)), "AES");
|
||||
var cipherBytes = segment.h.getBytes();
|
||||
//if IV doesn't pass, it must be created manually and filled with zeros
|
||||
if (cipherBytes.length != 16) Arrays.fill((cipherBytes = new byte[16]), (byte) 0x0);
|
||||
//if IV doesn't pass, it must be created manually
|
||||
if (cipherBytes.length != 16)
|
||||
cipherBytes = new byte[16];
|
||||
|
||||
var cipherIv = new IvParameterSpec(cipherBytes);
|
||||
cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIv);
|
||||
buff = cipher.doFinal(buff);
|
||||
CIPHER.init(Cipher.DECRYPT_MODE, cipherKey, cipherIv);
|
||||
buff = CIPHER.doFinal(buff);
|
||||
}
|
||||
IOUtils.writeToFile(new File(getTsesDir(track), filename), buff);
|
||||
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / size));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
|
||||
// copy on write
|
||||
final int len = total.length, n = buff.length;
|
||||
byte[] newBytes = Arrays.copyOf(total, len + n);
|
||||
System.arraycopy(buff, 0, newBytes, len, n);
|
||||
total = newBytes;
|
||||
|
||||
callback.onProgress(10 +
|
||||
Math.round(80.0f * progress.addAndGet(1) / size));
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
|
||||
public static String getTitle(MusicTrack track) {
|
||||
return track.f + (!TextUtils.isEmpty(track.g) ? + '(' + track.g + ')' : "");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user