cleanup code

This commit is contained in:
gdlbo
2023-05-05 20:48:39 +03:00
parent 6eae209d04
commit da52a7557f
4 changed files with 179 additions and 159 deletions
@@ -53,9 +53,9 @@ public class M3UDownloader implements ITrackDownloader {
return;
}
var request = new Request.a().b(track.D).a();
try {
var response = client.a(request).execute().a().g();
Request request = new Request.a().b(track.D).a();
String response = client.a(request).execute().a().g();
parse(response, outDir, callback, track, cache);
} catch (IOException e) {
e.printStackTrace();
@@ -64,70 +64,91 @@ public class M3UDownloader implements ITrackDownloader {
}
private void parse(String payload, File outDir, Callback callback, MusicTrack track, boolean cache) throws UnsupportedEncodingException {
if (cache) try {
if (cache) downloadThumbnails(track);
VKM3UParser parser = new VKM3UParser(track.D.substring(0, track.D.lastIndexOf("/") + 1), payload);
AtomicInteger progress = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = createFutures(parser, progress, callback, track);
var resultTs = getResultTsFile(track);
var resultMp3 = getResultMp3File(outDir, cache, track);
callback.onProgress(5);
var future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
future.join();
future.exceptionally(throwable -> {
callback.onFailure();
return null;
}).thenApply(v -> TSMerger.merge(getTsesDir(track), resultTs))
.thenApply(mergeResult -> mergeResult && FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track))
.thenApply(convertResult -> {
if (convertResult)
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / parser.mTransportStreams.size()));
else callback.onFailure();
return convertResult;
}).thenRun(() -> IOUtils.deleteRecursive(getTsesDir(track))).thenRun(() -> {
callback.onSuccess();
if (cache) CacheDatabaseDelegate.insertTrack(track);
});
}
private void downloadThumbnails(MusicTrack track) {
try {
ThumbnailDownloader.downloadThumbnails(track);
} catch (IOException e) {
e.printStackTrace();
}
}
VKM3UParser parser = new VKM3UParser(track.D.substring(0,track.D.lastIndexOf("/")+1),payload);
AtomicInteger progress = new AtomicInteger(0);
private List<CompletableFuture<Void>> createFutures(VKM3UParser parser, AtomicInteger progress, Callback callback, MusicTrack track) {
List<CompletableFuture<Void>> futures = new ArrayList<>(parser.mTransportStreams.size());
var tses = parser.mTransportStreams;
for (var i = 0; i < tses.size(); ++i) {
final var ts = tses.get(i);
final var filename = i + ".ts";
futures.add(CompletableFuture.runAsync(() -> processTransportStream(parser, ts, filename, progress, tses.size(), callback, track)));
}
return futures;
}
private void processTransportStream(VKM3UParser parser, TransportStream ts, String filename, AtomicInteger progress, int size, Callback callback, MusicTrack track) {
try {
InputStream mediaIs = IOUtils.openStream(ts.getMediaSegmentUri());
byte[] content;
if (ts.needDecoding()) {
InputStream keyPubIs = IOUtils.openStream(ts.getKeyURL());
content = IOUtils.decodeStream(mediaIs, IOUtils.readFully(keyPubIs));
keyPubIs.close();
} else {
content = IOUtils.readFully(mediaIs);
}
File tsDump = new File(getTsesDir(track), filename);
IOUtils.writeToFile(tsDump, content);
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / size));
callback.onSizeReceived((long) content.length * size, parser.mHeapSize);
mediaIs.close();
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException |
InvalidAlgorithmParameterException | InvalidKeyException e) {
callback.onFailure();
throw new RuntimeException(e);
}
}
private File getTsesDir(MusicTrack track) {
var filesDir = AndroidUtils.getGlobalContext().getFilesDir();
var tses=parser.mTransportStreams;
var tsesDir = new File(filesDir, String.valueOf(track.d));
tsesDir.mkdirs();
var resultTs = new File(tsesDir, "result.ts");
return tsesDir;
}
private File getResultTsFile(MusicTrack track) {
return new File(getTsesDir(track), "result.ts");
}
private File getResultMp3File(File outDir, boolean cache, MusicTrack track) {
var fileName = IOUtils.getValidFileName((cache ? "track" : track.C + " - " + getTitle(track)) + ".mp3");
var resultMp3 = new File(outDir, fileName);
callback.onProgress(5);
for (var i=0;i<tses.size();++i) {
final var ts=tses.get(i);
final var filename=i+".ts";
futures.add(CompletableFuture.runAsync(() -> {
InputStream mediaIs,keyPubIs;
mediaIs=keyPubIs=null;
try {
mediaIs=IOUtils.openStream(ts.getMediaSegmentUri());
byte[] content;
if (ts.needDecoding()) {
keyPubIs=IOUtils.openStream(ts.getKeyURL());
content=IOUtils.decodeStream(mediaIs,IOUtils.readFully(keyPubIs));
} else {
content=IOUtils.readFully(mediaIs);
}
File tsDump = new File(tsesDir,filename);
IOUtils.writeToFile(tsDump, content);
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / tses.size()));
callback.onSizeReceived((long) content.length * tses.size(), parser.mHeapSize);
mediaIs.close();
keyPubIs.close();
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException |
InvalidAlgorithmParameterException | InvalidKeyException e) {
callback.onFailure();
throw new RuntimeException(e);
}
}));
}
var future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
future.join();
future.exceptionally(throwable -> {
callback.onFailure();
return null;
}).thenApply(v -> TSMerger.merge(tsesDir, resultTs))
.thenApply(mergeResult -> mergeResult && FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track))
.thenApply(convertResult -> {
if (convertResult)
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / tses.size()));
else callback.onFailure();
return convertResult;
}).thenRun(() -> IOUtils.deleteRecursive(tsesDir)).thenRun(() -> {
callback.onSuccess();
if (cache) CacheDatabaseDelegate.insertTrack(track);
});
return new File(outDir, fileName);
}
// Initialization-on-demand
@@ -5,38 +5,32 @@ import android.text.TextUtils;
public class TransportStream {
private String mKeyUri;
private String mMediaSegmentUri;
private final String mMediaSegmentUri;
private String mMediaSegmentName;
public TransportStream(String baseURI,String mediaSegmentName)
{
this.mMediaSegmentUri=baseURI+mediaSegmentName;
public TransportStream(String baseURI, String mediaSegmentName) {
this.mMediaSegmentUri = baseURI + mediaSegmentName;
}
public TransportStream(String keyURI,String baseURI,String mediaSegmentName)
{
this.mKeyUri=keyURI;
this.mMediaSegmentUri=baseURI+mediaSegmentName;
this.mMediaSegmentName=mediaSegmentName;
public TransportStream(String keyURI, String baseURI, String mediaSegmentName) {
this.mKeyUri = keyURI;
this.mMediaSegmentUri = baseURI + mediaSegmentName;
this.mMediaSegmentName = mediaSegmentName;
}
public boolean needDecoding()
{
public boolean needDecoding() {
return !TextUtils.isEmpty(mKeyUri);
}
public String getKeyURL()
{
public String getKeyURL() {
return mKeyUri;
}
public String getMediaSegmentUri()
{
public String getMediaSegmentUri() {
return this.mMediaSegmentUri;
}
public String getMediaSegmentName()
{
public String getMediaSegmentName() {
return this.mMediaSegmentName;
}
}
@@ -9,52 +9,51 @@ import java.util.Scanner;
/**
* The parser for extended M3U playlist format used by VK.
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#ref-M3U">M3U</a>
**/
public class VKM3UParser {
//Playlist types
final static public int PLAYLIST_TYPE_VOD=0;
final static public int PLAYLIST_TYPE_EVENT=1;
static public int PLAYLIST_TYPE_VOD = 0;
static public int PLAYLIST_TYPE_EVENT = 1;
//region Extended M3U global extensions
public List<TransportStream> mTransportStreams = new ArrayList<>();
private final String mData;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.1.2">EXT-X-VERSION</a>
**/
public int mVersion=-1;
public int mVersion = -1;
/**
* The EXT-X-ALLOW-CACHE tag is used in protocol version 6 or lower.
* If the protocol version is upgraded by VK, we will not support that.
**/
public boolean mAllowCache=false;
public boolean mAllowCache = false;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.1">EXT-X-TARGETDURATION</a>
**/
public int mTargetDuration=-1;
public int mTargetDuration = -1;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.2">EXT-X-MEDIA-SEQUENCE</a>
**/
public int mMediaSequence=-1;
public int mMediaSequence = -1;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.3">EXT-X-DISCONTINUITY-SEQUENCE</a>
**/
public int mDiscontinuitySequence=-1;
public int mDiscontinuitySequence = -1;
//endregion
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.5">EXT-X-PLAYLIST-TYPE</a>
**/
public int mPlaylistType=-1;
public int mPlaylistType = -1;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.6">EXT-X-I-FRAMES-ONLY</a>
**/
public boolean mIFramesOnly=false;
//endregion
public boolean mIFramesOnly = false;
public long mHeapSize = 0;
public long mHeapSize=0;
final private String mData;
final public List<TransportStream> mTransportStreams = new ArrayList<>();
public VKM3UParser(String baseUri,String data) {
mData=data;
public VKM3UParser(String baseUri, String data) {
mData = data;
init(baseUri);
}
@@ -62,68 +61,67 @@ public class VKM3UParser {
return str.matches(".+\\.ts.*");
}
private void init(String baseUri)
{
if(TextUtils.isEmpty(mData))throw new NullPointerException("mData==null");
final var scanner=new Scanner(mData);
var line=scanner.nextLine();
if(!"#EXTM3U".equals(line))throw new IllegalStateException(String.format("Unknown initial M3U tag: %s",line));
//indicators for EXT-X-KEY and EXTINF tags
boolean aes128,extinf;
aes128=extinf=false;
/**
* VK follows own URI attribute for EXT-X-KEY tag.
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.2.4">EXT-X-KEY</a>
**/
String keyUri="";
while(scanner.hasNextLine())
{
line=scanner.nextLine();
if('#'==line.charAt(0))
{
if(line.startsWith("#EXT-X-VERSION:"))
mVersion=Integer.parseInt(line.substring(15));
else if(line.startsWith("#EXT-X-ALLOW-CACHE:"))
mAllowCache="YES".equalsIgnoreCase(line.substring(19));
else if(line.startsWith("#EXT-X-TARGETDURATION:"))
mTargetDuration=Integer.parseInt(line.substring(22));
else if(line.startsWith("#EXT-X-MEDIA-SEQUENCE:"))
mMediaSequence=Integer.parseInt(line.substring(22));
else if(line.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE:"))
mDiscontinuitySequence=Integer.parseInt(line.substring(30));
else if(line.startsWith("#EXT-X-PLAYLIST-TYPE:"))
switch (line.substring(21))
{
case "VOD"->mPlaylistType=PLAYLIST_TYPE_VOD;
case "EVENT"->mPlaylistType=PLAYLIST_TYPE_EVENT;
default->throw new IllegalStateException(String.format("Unknown playlist type: %s", line.substring(21)));
}
else if(line.startsWith("#EXT-X-I-FRAMES-ONLY"))
mIFramesOnly=true;
else if(line.startsWith("#EXT-X-KEY:"))
{
var substr=line.substring(11);
if((aes128=substr.contains("METHOD=AES-128")))
{
substr=substr.substring(substr.indexOf('"')+1,substr.lastIndexOf('"'));
//TODO: supposed endpoint
keyUri=substr.startsWith("http")?substr:baseUri+substr;
Log.d("keyURL",keyUri);
}
}
else if(line.startsWith("#EXTINF"))
extinf=true;
}
else if(extinf&&isTS(line))
{
final var ts=aes128
?new TransportStream(keyUri,baseUri,line)
:new TransportStream(baseUri,line);
mTransportStreams.add(ts);
aes128=extinf=false;
}
else throw new RuntimeException(String.format("Failed to parse: %s",line));
mHeapSize+=line.getBytes().length;
}
private void init(String baseUri) {
if (TextUtils.isEmpty(mData)) throw new NullPointerException("mData==null");
Scanner scanner = new Scanner(mData);
String line = scanner.nextLine();
if (!"#EXTM3U".equals(line)) throw new IllegalStateException(String.format("Unknown initial M3U tag: %s", line));
boolean aes128 = false;
String keyUri = "";
while (scanner.hasNextLine()) {
line = scanner.nextLine();
if ('#' == line.charAt(0)) {
parseTag(line);
if (line.startsWith("#EXT-X-KEY:") && line.contains("METHOD=AES-128")) {
aes128 = true;
keyUri = getKeyUri(line, baseUri);
}
} else if (isTS(line)) {
addTransportStream(aes128, keyUri, baseUri, line);
aes128 = false;
} else {
throw new RuntimeException(String.format("Failed to parse: %s", line));
}
mHeapSize += line.getBytes().length;;
}
}
private void parseTag(String line) {
if (line.startsWith("#EXT-X-VERSION:"))
mVersion = Integer.parseInt(line.substring(15));
else if (line.startsWith("#EXT-X-ALLOW-CACHE:"))
mAllowCache = "YES".equalsIgnoreCase(line.substring(19));
else if (line.startsWith("#EXT-X-TARGETDURATION:"))
mTargetDuration = Integer.parseInt(line.substring(22));
else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE:"))
mMediaSequence = Integer.parseInt(line.substring(22));
else if (line.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE:"))
mDiscontinuitySequence = Integer.parseInt(line.substring(30));
else if (line.startsWith("#EXT-X-PLAYLIST-TYPE:"))
parsePlaylistType(line.substring(21));
else if (line.startsWith("#EXT-X-I-FRAMES-ONLY"))
mIFramesOnly = true;
}
private void parsePlaylistType(String type) {
switch (type) {
case "VOD" -> mPlaylistType = PLAYLIST_TYPE_VOD;
case "EVENT" -> mPlaylistType = PLAYLIST_TYPE_EVENT;
default -> throw new IllegalStateException(String.format("Unknown playlist type: %s", type));
}
}
private String getKeyUri(String line, String baseUri) {
String substr = line.substring(11);
substr = substr.substring(substr.indexOf('"') + 1, substr.lastIndexOf('"'));
//TODO: supposed endpoint
return substr.startsWith("http") ? substr : baseUri + substr;
}
private void addTransportStream(boolean aes128, String keyUri, String baseUri, String line) {
TransportStream ts = aes128
? new TransportStream(keyUri, baseUri, line)
: new TransportStream(baseUri, line);
mTransportStreams.add(ts);
}
}
@@ -26,14 +26,7 @@ public class TSMerger {
var mergedByteArrayStream = new ByteArrayOutputStream();
//0.ts,1.ts,...
Arrays.sort(tsesList, (ts1,ts2) -> {
final var ts1Name=ts1.getName();
final var ts2Name=ts2.getName();
final var i1=Integer.parseInt(ts1Name.substring(0,ts1Name.indexOf('.',1)));
final var i2=Integer.parseInt(ts2Name.substring(0,ts2Name.indexOf('.',1)));
return Integer.compare(i1,i2);
});
sortTsesList(tsesList);
for (var file : tsesList) {
if (file.getName().endsWith(".ts")) {
@@ -46,6 +39,20 @@ public class TSMerger {
}
}
return writeOutputFile(out, mergedByteArrayStream);
}
private static void sortTsesList(File[] tsesList) {
Arrays.sort(tsesList, (ts1, ts2) -> {
String ts1Name = ts1.getName();
String ts2Name = ts2.getName();
int i1 = Integer.parseInt(ts1Name.substring(0, ts1Name.indexOf('.', 1)));
int i2 = Integer.parseInt(ts2Name.substring(0, ts2Name.indexOf('.', 1)));
return Integer.compare(i1, i2);
});
}
private static boolean writeOutputFile(File out, ByteArrayOutputStream mergedByteArrayStream) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
Files.write(out.toPath(), mergedByteArrayStream.toByteArray());