Merge remote-tracking branch 'origin/vtl-remaster' into vtl-remaster
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user