Merge remote-tracking branch 'origin/vtl-remaster' into vtl-remaster

This commit is contained in:
gdlbo
2022-07-10 01:08:41 +03:00
6 changed files with 463 additions and 0 deletions
@@ -0,0 +1,8 @@
package ru.vtosters.lite.downloaders.music;
public interface Callback {
void onProgress(int progress);
void onSuccess();
void onFailure();
void onSizeReceived(long size, long header);
}
@@ -0,0 +1,6 @@
package ru.vtosters.lite.downloaders.music;
public enum KeyType {
NONE,
AES128
}
@@ -0,0 +1,179 @@
package ru.vtosters.lite.downloaders.music;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import ru.vtosters.lite.net.NetClient;
import ru.vtosters.lite.net.NetRequest;
public class M3U8 {
private final String url;
private final OutputStream output;
private final Callback callback;
private final int returnChunk;
public M3U8(String url, String savedFileName, OutputStream output, Callback callback, int chunksNum) {
try {
this.url = url;
var file = new File(savedFileName).getAbsoluteFile();
file.getParentFile().mkdirs();
this.output = new FileOutputStream(file);
this.callback = callback;
this.returnChunk = chunksNum - 1;
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
public static InputStream getStream(NetClient client, URL url) {
var request = new NetRequest.Builder()
.url(url.toString())
.build();
try {
var response = client.newCall(request).execute();
if (!response.isSuccessful()) {
return null;
}
return response.getDataStream();
} catch (IOException e) {
return null;
}
}
// пожалуйста не смотрите сюда
public boolean run(NetClient client) {
try {
URL mediaURL;
URL m3u8Url = new URL(url);
int headerSize = 0;
try (InputStream is = getStream(client, m3u8Url)) {
if (is == null) {
return false;
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
String line;
ArrayList<Map.Entry<Long, URL>> urls = new ArrayList<>();
long newUrl = 0;
while ((line = br.readLine()) != null) {
headerSize += line.getBytes().length;
line = line.trim();
Property property = Property.checkProperty(line);
if (property == null) {
newUrl = 0;
} else if (property.type.equals("EXT-X-STREAM-INF")) {
if (property.properties.get("BANDWIDTH") != null)
newUrl = Long.parseLong(property.properties.get("BANDWIDTH"));
else newUrl = 1;
} else if (property.type.equals("FILE") && newUrl > 0) {
urls.add(new java.util.AbstractMap.SimpleEntry<>(newUrl, new URL(m3u8Url, line)));
newUrl = 0;
} else {
newUrl = 0;
}
}
Collections.sort(urls, new Comparator<>() {
@Override
public int compare(Map.Entry<Long, URL> o1, Map.Entry<Long, URL> o2) {
return -Long.compare(o1.getKey(), o2.getKey());
}
});
if (urls.size() > 0) {
m3u8Url = urls.get(0).getValue();
}
}
this.callback.onProgress(5);
mediaURL = m3u8Url;
ArrayList<TSDownload> tsDownloads = new ArrayList<>();
var iis = getStream(client, mediaURL);
if (iis == null) {
return false;
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(iis))) {
var type = KeyType.NONE;
var key = new byte[16];
var iv = new byte[16];
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
var property = Property.checkProperty(line);
if (property == null)
continue;
else if (property.type.equals("EXT-X-KEY")) {
if ("NONE".equals(property.properties.get("METHOD"))) {
type = KeyType.NONE;
continue;
}
type = KeyType.AES128;
var keyUrl = new URL(mediaURL, property.properties.get("URI"));
var keyStream = getStream(client, keyUrl);
if (keyStream == null) {
return false;
}
try (InputStream i = keyStream) {
var keyLength = is.read(key);
if (keyLength != key.length) {
throw new RuntimeException("wtf");
}
}
if (property.properties.get("IV") != null) {
String ivstr = property.properties.get("IV");
ivstr = ivstr.substring(2);
for (int i = 0; i < iv.length; i++) {
iv[i] = (byte) Integer.parseInt(ivstr.substring(i * 2, (i + 1) * 2), 16);
}
} else {
Arrays.fill(iv, (byte) 0);
}
} else if (property.type.equals("FILE")) {
var url = new URL(mediaURL, line);
tsDownloads.add(new TSDownload(url, type, key, iv));
for (var i = iv.length; i > 0; i--) {
iv[i - 1] = (byte) (iv[i - 1] + 1);
if (iv[i - 1] != 0) {
break;
}
}
}
}
}
this.callback.onProgress(10);
try (var fileStream = output) {
for (var download : tsDownloads) {
if (download.start(client)) {
fileStream.write(download.getData());
this.callback.onSizeReceived((long) download.getData().length * tsDownloads.size(), headerSize);
this.callback.onProgress(10 + Math.round(80.0f * tsDownloads.indexOf(download) / tsDownloads.size()));
if (this.returnChunk == tsDownloads.indexOf(download)) {
return true;
} else {
return false;
}
}
}
}
}
} catch (Exception e) {
Log.e("VTosters Lite", e.toString());
return false;
}
return true;
}
}
@@ -0,0 +1,140 @@
package ru.vtosters.lite.downloaders.music;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class Property {
public String type;
public Map<String, String> properties;
public String[] values;
public String value;
@NonNull
public String toString() {
StringBuilder sb = new StringBuilder("Property type = ");
sb.append(type);
if (properties != null) {
sb.append(", properties = { ");
for (Map.Entry<String, String> i : properties.entrySet()) {
sb.append(i.getKey());
sb.append(" = ");
sb.append(i.getValue());
sb.append(", ");
}
sb.append("}");
}
if (values != null) {
sb.append(", values = [ ");
for (String i : values) {
sb.append(i);
sb.append(", ");
}
sb.append("]");
}
return sb.toString();
}
public static Property checkProperty(String line) {
Property property = parseLine(line);
if (property.type.equals("FILE")) return property;
if (property.type.equals("EXT-X-STREAM-INF") && property.properties != null) {
if (property.properties.get("BANDWIDTH") != null) {
try {
Long.parseLong(property.properties.get("BANDWIDTH"));
} catch (NumberFormatException e) {
return null;
}
}
return property;
}
if (property.type.equals("EXTINF") && property.values != null) {
// #EXTINF:10.23,
try {
Double.parseDouble(property.values[0]);
} catch (NumberFormatException e) {
return null;
}
return property;
}
if (property.type.equals("EXT-X-KEY") && property.properties != null) {
// #EXT-X-KEY:METHOD=AES-128,URI=\"(.*)\",IV=0[xX](.{32})
boolean aes = "AES-128".equals(property.properties.get("METHOD"));
if (!aes && !"NONE".equals(property.properties.get("METHOD"))) {
return null;
}
if (!property.properties.containsKey("URI") && aes) {
return null;
}
if (property.properties.containsKey("IV") && !property.properties.get("IV").matches("0[xX](.{32})")) {
return null;
}
return property;
}
return null;
}
public static Property parseLine(String line) {
Property ret = new Property();
if (!line.startsWith("#")) {
ret.type = "FILE";
ret.value = line;
return ret;
}
String[] types = line.split(":", 2);
ret.type = types[0].substring(1);
if (types.length == 1) {
return ret;
}
ret.value = types[1];
ret.values = splitProperty(types[1]);
ret.properties = new HashMap<>();
for (String p : ret.values) {
if (p.length() > 0) {
String[] ps = p.split("=", 2);
if (ps.length == 1) {
ret.properties.put(p, null);
} else {
String k = ps[0];
String v = ps[1];
if (v.charAt(0) == '"' && v.charAt(v.length() - 1) == '"') {
v = v.substring(1, v.length() - 1);
}
ret.properties.put(k, v);
}
}
}
return ret;
}
public static String[] splitProperty(String line) {
ArrayList<String> list = new ArrayList<>();
boolean escape = false;
boolean quote = false;
StringBuilder sb = new StringBuilder();
for (char c : line.toCharArray()) {
if (escape) {
escape = false;
} else if (quote) {
if (c == '"') quote = false;
} else {
if (c == ',') {
list.add(sb.toString());
sb.delete(0, sb.length());
continue;
}
if (c == '\\') escape = true;
if (c == '"') quote = true;
}
sb.append(c);
}
list.add(sb.toString());
return list.toArray(new String[]{});
}
}
@@ -0,0 +1,112 @@
package ru.vtosters.lite.downloaders.music;
import android.util.Log;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import ru.vtosters.lite.downloaders.music.utils.ByteUtils;
import ru.vtosters.lite.net.NetClient;
public class TSDownload {
private static final int TS_PACKET_SIZE = 188;
private final URL url;
private final KeyType type;
private final byte[] key;
private final byte[] iv;
private byte[] bytes;
public TSDownload(URL url, KeyType type, byte[] key, byte[] iv) {
this.url = url;
this.type = type;
this.key = key.clone();
this.iv = iv.clone();
}
private static void closeQuietly(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException ignored) {
}
}
private static Cipher getAesCp(byte[] key, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Key skey = new SecretKeySpec(key, "AES");
IvParameterSpec param = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, skey, param);
return cipher;
}
public URL getUrl() {
return url;
}
public KeyType getType() {
return type;
}
public byte[] getKey() {
return key;
}
public byte[] getIv() {
return iv;
}
public boolean start(NetClient client) {
try {
InputStream iis = M3U8.getStream(client, getUrl());
if (iis == null) {
return false;
}
KeyType type = getType();
byte[] bytes;
byte[] buffer = new byte[4096];
if (type == KeyType.AES128) {
CipherInputStream stream = new CipherInputStream(iis, getAesCp(key, iv));
bytes = ByteUtils.toByteArray(stream);
closeQuietly(stream);
} else if (type == KeyType.NONE) {
bytes = ByteUtils.toByteArray(iis);
} else {
throw new IllegalStateException();
}
if (bytes.length % TS_PACKET_SIZE != 0) {
throw new IllegalStateException();
}
for (int i = 0; i < bytes.length; i = i + TS_PACKET_SIZE) {
if (bytes[i] != 0x47) {
throw new IllegalStateException();
}
}
this.bytes = bytes;
} catch (Exception e) {
Log.w("vk_m3u8", e);
return false;
}
return true;
}
public byte[] getData() {
return bytes;
}
}
@@ -0,0 +1,18 @@
package ru.vtosters.lite.downloaders.music.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class ByteUtils {
// copilot did something useful wow wow wow
public static byte[] toByteArray(InputStream is) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
return os.toByteArray();
}
}