cleanup code
This commit is contained in:
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user