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 io.reactivex.Scheduler;
|
||||||
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.ForkJoinPool;
|
||||||
|
|
||||||
public class VTExecutors {
|
public class VTExecutors {
|
||||||
public static ExecutorService getMusicDownloadExecutor() {
|
public static ExecutorService getMusicDownloadExecutor() {
|
||||||
@@ -32,7 +32,20 @@ public class VTExecutors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static class LazyHolder {
|
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;
|
||||||
import com.google.android.exoplayer2.source.hls.playlist.f.a;
|
import com.google.android.exoplayer2.source.hls.playlist.f.a;
|
||||||
import com.vk.dto.music.MusicTrack;
|
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.cache.MusicCacheImpl;
|
||||||
import ru.vtosters.lite.music.converter.ts.FFMpeg;
|
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.Callback;
|
||||||
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
|
||||||
import ru.vtosters.lite.utils.AndroidUtils;
|
import ru.vtosters.lite.utils.AndroidUtils;
|
||||||
import ru.vtosters.lite.utils.IOUtils;
|
import ru.vtosters.lite.utils.IOUtils;
|
||||||
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
|
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public class M3UDownloader
|
public class M3UDownloader
|
||||||
implements ITrackDownloader {
|
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() {
|
public static M3UDownloader getInstance() {
|
||||||
return Holder.INSTANCE;
|
return Holder.INSTANCE;
|
||||||
}
|
}
|
||||||
@@ -47,8 +62,11 @@ public class M3UDownloader
|
|||||||
var resultTs = getResultTsFile(track);
|
var resultTs = getResultTsFile(track);
|
||||||
var resultMp3 = getResultMp3File(outDir, cache, track);
|
var resultMp3 = getResultMp3File(outDir, cache, track);
|
||||||
try {
|
try {
|
||||||
CompletableFuture.allOf(createFutures(baseUri, segments, progress, callback, track))
|
|
||||||
.thenRun(() -> {
|
byte[] buff = downloadTs(baseUri, segments, progress, callback);
|
||||||
|
|
||||||
|
IOUtils.writeToFile(resultTs, buff);
|
||||||
|
|
||||||
if (cache) {
|
if (cache) {
|
||||||
try {
|
try {
|
||||||
ThumbnailDownloader.downloadThumbnails(track);
|
ThumbnailDownloader.downloadThumbnails(track);
|
||||||
@@ -56,85 +74,65 @@ public class M3UDownloader
|
|||||||
throw new RuntimeException("Failed to download thumbs", 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 {
|
try {
|
||||||
FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track);
|
FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
throw new RuntimeException("FFmpeg error", e);
|
throw new RuntimeException("FFmpeg error", e);
|
||||||
}
|
}
|
||||||
})
|
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / segments.size()));
|
||||||
.thenRun(() -> callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / segments.size())))
|
|
||||||
.thenRun(() -> {
|
|
||||||
if (cache) {
|
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) {
|
|
||||||
Log.e("M3UDownloader", "Failed to download track", e.getCause());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 void processSegment(
|
|
||||||
String baseUri,
|
|
||||||
a segment,
|
|
||||||
String filename,
|
|
||||||
AtomicInteger progress,
|
|
||||||
int size,
|
|
||||||
Callback callback,
|
|
||||||
MusicTrack track) {
|
|
||||||
try {
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[]
|
||||||
|
downloadTs(String baseUri, List<a> segments,
|
||||||
|
AtomicInteger progress, Callback callback
|
||||||
|
) throws IOException, InvalidAlgorithmParameterException,
|
||||||
|
InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
|
||||||
|
byte[] total = new byte[0];
|
||||||
|
|
||||||
|
int size = segments.size();
|
||||||
|
for (var segment : segments) {
|
||||||
var buff = IOUtils.readFully(IOUtils.openStream(baseUri + segment.a/*url*/));
|
var buff = IOUtils.readFully(IOUtils.openStream(baseUri + segment.a/*url*/));
|
||||||
if (!TextUtils.isEmpty(segment.g)) {
|
if (!TextUtils.isEmpty(segment.g)) {
|
||||||
var cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
|
|
||||||
var cipherKey = new SecretKeySpec(IOUtils.readFully(IOUtils.openStream(segment.g)), "AES");
|
var cipherKey = new SecretKeySpec(IOUtils.readFully(IOUtils.openStream(segment.g)), "AES");
|
||||||
var cipherBytes = segment.h.getBytes();
|
var cipherBytes = segment.h.getBytes();
|
||||||
//if IV doesn't pass, it must be created manually and filled with zeros
|
//if IV doesn't pass, it must be created manually
|
||||||
if (cipherBytes.length != 16) Arrays.fill((cipherBytes = new byte[16]), (byte) 0x0);
|
if (cipherBytes.length != 16)
|
||||||
|
cipherBytes = new byte[16];
|
||||||
|
|
||||||
var cipherIv = new IvParameterSpec(cipherBytes);
|
var cipherIv = new IvParameterSpec(cipherBytes);
|
||||||
cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIv);
|
CIPHER.init(Cipher.DECRYPT_MODE, cipherKey, cipherIv);
|
||||||
buff = cipher.doFinal(buff);
|
buff = CIPHER.doFinal(buff);
|
||||||
}
|
}
|
||||||
IOUtils.writeToFile(new File(getTsesDir(track), filename), buff);
|
|
||||||
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / size));
|
// copy on write
|
||||||
} catch (Exception e) {
|
final int len = total.length, n = buff.length;
|
||||||
throw new RuntimeException(e);
|
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) {
|
public static String getTitle(MusicTrack track) {
|
||||||
return track.f + (!TextUtils.isEmpty(track.g) ? + '(' + track.g + ')' : "");
|
return track.f + (!TextUtils.isEmpty(track.g) ? + '(' + track.g + ')' : "");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user