From da52a7557fca443ebecb904c8d214b6bae69f9f8 Mon Sep 17 00:00:00 2001 From: gdlbo <41114720+gdlbo@users.noreply.github.com> Date: Fri, 5 May 2023 20:14:51 +0300 Subject: [PATCH] cleanup code --- .../ru/vtosters/lite/music/M3UDownloader.java | 129 ++++++++------ .../vtosters/lite/music/TransportStream.java | 28 ++-- .../ru/vtosters/lite/music/VKM3UParser.java | 158 +++++++++--------- .../lite/music/converter/ts/TSMerger.java | 23 ++- 4 files changed, 179 insertions(+), 159 deletions(-) diff --git a/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java b/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java index 54c88503a..79b4bca4b 100644 --- a/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java +++ b/app/src/main/java/ru/vtosters/lite/music/M3UDownloader.java @@ -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> 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> createFutures(VKM3UParser parser, AtomicInteger progress, Callback callback, MusicTrack track) { List> 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 { - 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 diff --git a/app/src/main/java/ru/vtosters/lite/music/TransportStream.java b/app/src/main/java/ru/vtosters/lite/music/TransportStream.java index 12404a398..62b68c8a8 100644 --- a/app/src/main/java/ru/vtosters/lite/music/TransportStream.java +++ b/app/src/main/java/ru/vtosters/lite/music/TransportStream.java @@ -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; } } diff --git a/app/src/main/java/ru/vtosters/lite/music/VKM3UParser.java b/app/src/main/java/ru/vtosters/lite/music/VKM3UParser.java index 9bc3ed6c4..03b210c98 100644 --- a/app/src/main/java/ru/vtosters/lite/music/VKM3UParser.java +++ b/app/src/main/java/ru/vtosters/lite/music/VKM3UParser.java @@ -9,52 +9,51 @@ import java.util.Scanner; /** * The parser for extended M3U playlist format used by VK. + * * @see M3U **/ 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 mTransportStreams = new ArrayList<>(); + private final String mData; /** * @see EXT-X-VERSION **/ - 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 EXT-X-TARGETDURATION **/ - public int mTargetDuration=-1; + public int mTargetDuration = -1; /** * @see EXT-X-MEDIA-SEQUENCE **/ - public int mMediaSequence=-1; + public int mMediaSequence = -1; /** * @see EXT-X-DISCONTINUITY-SEQUENCE **/ - public int mDiscontinuitySequence=-1; + public int mDiscontinuitySequence = -1; + //endregion /** * @see EXT-X-PLAYLIST-TYPE **/ - public int mPlaylistType=-1; + public int mPlaylistType = -1; /** * @see EXT-X-I-FRAMES-ONLY **/ - public boolean mIFramesOnly=false; - //endregion + public boolean mIFramesOnly = false; + public long mHeapSize = 0; - public long mHeapSize=0; - - final private String mData; - final public List 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 EXT-X-KEY - **/ - 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); } } diff --git a/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java b/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java index a05b8d2a5..f4c1ec2c3 100644 --- a/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java +++ b/app/src/main/java/ru/vtosters/lite/music/converter/ts/TSMerger.java @@ -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());