/*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.frostwire.search.extractors;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import jd.http.Browser;
import jd.http.Request;
import jd.nutils.encoding.Encoding;
import jd.parser.Regex;
import jd.parser.html.Form;
import jd.parser.html.Form.MethodType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.frostwire.search.FileSearchResult;
import com.frostwire.util.HttpClient;
import com.frostwire.util.HttpClientFactory;
/**
* @author gubatron
* @author aldenml
*
*/
public final class YouTubeExtractor {
private static final Logger LOG = LoggerFactory.getLogger(YouTubeExtractor.class);
private static final Pattern FILENAME_PATTERN = Pattern.compile("<meta name=\"title\" content=\"(.*?)\">", Pattern.CASE_INSENSITIVE);
private static final String UNSUPPORTEDRTMP = "itag%2Crtmpe%2";
private static final Map<Integer, Format> FORMATS = buildFormats();
// using the signature decoding per running session
private static YouTubeSig YT_SIG;
public List<LinkInfo> extract(String videoUrl, boolean testConnection) {
try {
Thread.sleep(100);
Browser br = new Browser();
HashMap<Integer, String> LinksFound = getLinks(videoUrl, false, br);
checkError(videoUrl, br, LinksFound);
String filename = LinksFound.remove(-1);
filename = cleanupFilename(filename);
SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.yyyy", Locale.ENGLISH);
String dateStr = br.getRegex("id=\"eow-date\" class=\"watch-video-date\" >(\\d{2}\\.\\d{2}\\.\\d{4})</span>").getMatch(0);
if (dateStr == null) {
formatter = new SimpleDateFormat("dd MMM yyyy", Locale.ENGLISH);
dateStr = br.getRegex("class=\"watch-video-date\" >([ ]+)?(\\d{1,2} [A-Za-z]{3} \\d{4})</span>").getMatch(1);
}
Date date = dateStr != null ? formatter.parse(dateStr) : new Date();
String videoId = getVideoID(videoUrl);
String channelName = br.getRegex("feature=watch\"[^>]+dir=\"ltr[^>]+>(.*?)</a>(\\s+)?<span class=\"yt-user").getMatch(0);
String userName = br.getRegex("temprop=\"url\" href=\"http://(www\\.)?youtube\\.com/user/([^<>\"]*?)\"").getMatch(1);
ThumbnailLinks thumbnailLinks = createThumbnailLink(videoId);
List<LinkInfo> infos = new LinkedList<LinkInfo>();
if (!testConnection || testConnection(br, getFirstLink(LinksFound))) {
for (int fmt : LinksFound.keySet()) {
Format format = FORMATS.get(fmt);
if (format == null) {
continue;
}
String link = LinksFound.get(fmt);
LinkInfo info = new LinkInfo(link, fmt, filename, FileSearchResult.UNKNOWN_SIZE, date, videoId, userName, channelName, thumbnailLinks, format);
infos.add(info);
}
}
return infos;
} catch (Throwable e) {
throw new ExtractorException("General extractor error", e);
}
}
private boolean testConnection(Browser br, String link) {
boolean connected = false;
try {
if (br.openGetConnection(link).getResponseCode() == 200) {
br.getHttpConnection().getLongContentLength();
connected = true;
}
} catch (Throwable e) {
log("Failed link url: " + link);
} finally {
try {
br.getHttpConnection().disconnect();
} catch (final Throwable e) {
}
}
return connected;
}
private String getFirstLink(Map<Integer, String> linksFound){
for (int fmt : linksFound.keySet()) {
Format format = FORMATS.get(fmt);
if (format == null) {
continue;
}
return linksFound.get(fmt);
}
return null;
}
private void checkError(String videoUrl, Browser br, HashMap<Integer, String> LinksFound) {
String error = br.getRegex("<div id=\"unavailable\\-message\" class=\"\">[\t\n\r ]+<span class=\"yt\\-alert\\-vertical\\-trick\"></span>[\t\n\r ]+<div class=\"yt\\-alert\\-message\">([^<>\"]*?)</div>").getMatch(0);
if (error == null) {
error = br.getRegex("reason=([^<>\"/]*?)(\\&|$)").getMatch(0);
}
if (br.containsHTML(UNSUPPORTEDRTMP)) {
error = "RTMP video download isn't supported yet!";
}
if ((LinksFound == null || LinksFound.isEmpty()) && error != null) {
error = Encoding.urlDecode(error, false);
if (error != null) {
error = error.trim();
}
throw new ExtractorException("Reasig: " + error.trim());
}
}
private HashMap<Integer, String> getLinks(final String video, final boolean prem, Browser br) throws Exception {
br.setFollowRedirects(true);
/* this cookie makes html5 available and skip controversy check */
br.setCookie("youtube.com", "PREF", "f2=40100000&hl=en-GB");
br.getHeaders().put("User-Agent", "Wget/1.12");
br.getPage(video);
if (br.containsHTML("id=\"unavailable-submessage\" class=\"watch-unavailable-submessage\"")) {
return null;
}
String videoId = new Regex(video, "watch\\?v=([\\w_\\-]+)").getMatch(0);
boolean fileNameFound = false;
String filename = videoId;
if (br.containsHTML("&title=")) {
filename = Encoding.htmlDecode(br.getRegex("&title=([^&$]+)").getMatch(0).replaceAll("\\+", " ").trim());
fileNameFound = true;
}
String url = br.getURL();
boolean ythack = false;
if (url != null && !url.equals(video)) {
/* age verify with activated premium? */
if (url.toLowerCase(Locale.ENGLISH).indexOf("youtube.com/verify_age?next_url=") != -1) {
//verifyAge = true;
}
if (url.toLowerCase(Locale.ENGLISH).indexOf("youtube.com/verify_age?next_url=") != -1 && prem) {
final String session_token = br.getRegex("onLoadFunc.*?gXSRF_token = '(.*?)'").getMatch(0);
final LinkedHashMap<String, String> p = Request.parseQuery(url);
final String next = p.get("next_url");
final Form form = new Form();
form.setAction(url);
form.setMethod(MethodType.POST);
form.put("next_url", "%2F" + next.substring(1));
form.put("action_confirm", "Confirm+Birth+Date");
form.put("session_token", Encoding.urlEncode(session_token));
br.submitForm(form);
if (br.getCookie("http://www.youtube.com", "is_adult") == null) {
return null;
}
} else if (url.toLowerCase(Locale.ENGLISH).indexOf("youtube.com/index?ytsession=") != -1 || url.toLowerCase(Locale.ENGLISH).indexOf("youtube.com/verify_age?next_url=") != -1 && !prem) {
ythack = true;
br.getPage("http://www.youtube.com/get_video_info?video_id=" + videoId);
if (br.containsHTML("&title=") && fileNameFound == false) {
filename = Encoding.htmlDecode(br.getRegex("&title=([^&$]+)").getMatch(0).replaceAll("\\+", " ").trim());
fileNameFound = true;
}
} else if (url.toLowerCase(Locale.ENGLISH).indexOf("google.com/accounts/servicelogin?") != -1) {
// private videos
return null;
}
}
Form forms[] = br.getForms();
if (forms != null) {
for (Form form : forms) {
if (form.getAction() != null && form.getAction().contains("verify_age")) {
log("Verify Age");
br.submitForm(form);
break;
}
}
}
String html5player = br.getRegex("(?s)(html5player\\-.+?\\.js)").getMatch(0);
YouTubeSig ytSig = getYouTubeSig("http://s.ytimg.com/yts/jsbin/" + html5player);
/* html5_fmt_map */
if (br.getRegex(FILENAME_PATTERN).count() != 0 && fileNameFound == false) {
filename = Encoding.htmlDecode(br.getRegex(FILENAME_PATTERN).getMatch(0).trim());
fileNameFound = true;
}
return parseLinks(br, video, filename, ythack, false, ytSig);
}
private HashMap<Integer, String> parseLinks(Browser br, final String videoURL, String filename, boolean ythack, boolean tryGetDetails, YouTubeSig ytSig) throws Exception {
final HashMap<Integer, String> links = new HashMap<Integer, String>();
String html5_fmt_map = br.getRegex("\"html5_fmt_map\": \\[(.*?)\\]").getMatch(0);
if (html5_fmt_map != null) {
String[] html5_hits = new Regex(html5_fmt_map, "\\{(.*?)\\}").getColumn(0);
if (html5_hits != null) {
for (String hit : html5_hits) {
String hitUrl = new Regex(hit, "url\": \"(http:.*?)\"").getMatch(0);
String hitFmt = new Regex(hit, "itag\": (\\d+)").getMatch(0);
if (hitUrl != null && hitFmt != null) {
hitUrl = unescape(hitUrl.replaceAll("\\\\/", "/"));
links.put(Integer.parseInt(hitFmt), Encoding.htmlDecode(Encoding.urlDecode(hitUrl, true)));
}
}
}
} else {
/* new format since ca. 1.8.2011 */
html5_fmt_map = br.getRegex("\"url_encoded_fmt_stream_map\": \"(.*?)\"").getMatch(0);
if (html5_fmt_map == null) {
html5_fmt_map = br.getRegex("url_encoded_fmt_stream_map=(.*?)(&|$)").getMatch(0);
if (html5_fmt_map != null) {
html5_fmt_map = html5_fmt_map.replaceAll("%2C", ",");
if (!html5_fmt_map.contains("url=")) {
html5_fmt_map = html5_fmt_map.replaceAll("%3D", "=");
html5_fmt_map = html5_fmt_map.replaceAll("%26", "&");
}
}
}
if (html5_fmt_map != null && !html5_fmt_map.contains("signature") && !html5_fmt_map.contains("sig") && !html5_fmt_map.contains("s=")) {
Thread.sleep(5000);
br.clearCookies("youtube.com");
return null;
}
if (html5_fmt_map != null) {
HashMap<Integer, String> ret = parseLinks(html5_fmt_map, ytSig);
if (ret.size() == 0)
return links;
links.putAll(ret);
if (true) {
/* not playable by vlc */
/* check for adaptive fmts */
String adaptive = br.getRegex("\"adaptive_fmts\": \"(.*?)\"").getMatch(0);
ret = parseLinks(adaptive, ytSig);
links.putAll(ret);
}
} else {
if (br.containsHTML("reason=Unfortunately"))
return null;
if (tryGetDetails == true) {
br.getPage("http://www.youtube.com/get_video_info?el=detailpage&video_id=" + getVideoID(videoURL));
return parseLinks(br, videoURL, filename, ythack, false, ytSig);
} else {
return null;
}
}
}
/* normal links */
final HashMap<String, String> fmt_list = new HashMap<String, String>();
String fmt_list_str = "";
if (ythack) {
fmt_list_str = (br.getMatch("&fmt_list=(.+?)&") + ",").replaceAll("%2F", "/").replaceAll("%2C", ",");
} else {
fmt_list_str = (br.getMatch("\"fmt_list\":\\s+\"(.+?)\",") + ",").replaceAll("\\\\/", "/");
}
final String fmt_list_map[][] = new Regex(fmt_list_str, "(\\d+)/(\\d+x\\d+)/\\d+/\\d+/\\d+,").getMatches();
for (final String[] fmt : fmt_list_map) {
fmt_list.put(fmt[0], fmt[1]);
}
if (links.size() == 0 && ythack) {
/* try to find fallback links */
String urls[] = br.getRegex("url%3D(.*?)($|%2C)").getColumn(0);
int index = 0;
for (String vurl : urls) {
String hitUrl = new Regex(vurl, "(.*?)%26").getMatch(0);
String hitQ = new Regex(vurl, "%26quality%3D(.*?)%").getMatch(0);
if (hitUrl != null && hitQ != null) {
hitUrl = unescape(hitUrl.replaceAll("\\\\/", "/"));
if (fmt_list_map.length >= index) {
links.put(Integer.parseInt(fmt_list_map[index][0]), Encoding.htmlDecode(Encoding.urlDecode(hitUrl, false)));
index++;
}
}
}
}
if (filename != null && links != null && !links.isEmpty()) {
links.put(-1, filename);
}
return links;
}
private HashMap<Integer, String> parseLinks(String html5_fmt_map, YouTubeSig ytSig) {
final HashMap<Integer, String> links = new HashMap<Integer, String>();
if (html5_fmt_map != null) {
if (html5_fmt_map.contains(UNSUPPORTEDRTMP)) {
return links;
}
String[] html5_hits = new Regex(html5_fmt_map, "(.*?)(,|$)").getColumn(0);
if (html5_hits != null) {
for (String hit : html5_hits) {
hit = unescape(hit);
String hitUrl = new Regex(hit, "url=(http.*?)(\\&|$)").getMatch(0);
String sig = new Regex(hit, "url=http.*?(\\&|$)(sig|signature)=(.*?)(\\&|$)").getMatch(2);
if (sig == null)
sig = new Regex(hit, "(sig|signature)=(.*?)(\\&|$)").getMatch(1);
if (sig == null)
sig = new Regex(hit, "(sig|signature)%3D(.*?)%26").getMatch(1);
if (sig == null) {
String temp = new Regex(hit, "s=(.*?)(\\&|$)").getMatch(0);
sig = ytSig != null && temp != null ? ytSig.calc(temp) : decryptSignature(temp);
}
String hitFmt = new Regex(hit, "itag=(\\d+)").getMatch(0);
if (hitUrl != null && hitFmt != null) {
hitUrl = unescape(hitUrl.replaceAll("\\\\/", "/"));
if (hitUrl.startsWith("http%253A")) {
hitUrl = Encoding.htmlDecode(hitUrl);
}
String inst = null;
if (hitUrl.contains("sig")) {
inst = Encoding.htmlDecode(Encoding.urlDecode(hitUrl, true));
} else {
inst = Encoding.htmlDecode(Encoding.urlDecode(hitUrl, true) + "&signature=" + sig);
}
links.put(Integer.parseInt(hitFmt), inst);
}
}
}
}
return links;
}
private String getVideoID(String URL) {
String vuid = new Regex(URL, "v=([A-Za-z0-9\\-_]+)").getMatch(0);
if (vuid == null) {
vuid = new Regex(URL, "(v|embed)/([A-Za-z0-9\\-_]+)").getMatch(1);
}
return vuid;
}
/**
* thx to youtube-dl
*
* @param s
* @return
*/
private String decryptSignature(String s) {
if (s == null)
return s;
StringBuilder sb = new StringBuilder();
log("SigLength: " + s.length());
if (s.length() == 93) {
sb.append(new StringBuilder(s.substring(30, 87)).reverse());
sb.append(s.charAt(88));
sb.append(new StringBuilder(s.substring(6, 29)).reverse());
} else if (s.length() == 92) {
sb.append(s.charAt(25));
sb.append(s.substring(3, 25));
sb.append(s.charAt(0));
sb.append(s.substring(26, 42));
sb.append(s.charAt(79));
sb.append(s.substring(43, 79));
sb.append(s.charAt(91));
sb.append(s.substring(80, 83));
} else if (s.length() == 91) {
sb.append(new StringBuilder(s.substring(28, 85)).reverse());
sb.append(s.charAt(86));
sb.append(new StringBuilder(s.substring(6, 27)).reverse());
} else if (s.length() == 90) {
sb.append(s.charAt(25));
sb.append(s.substring(3, 25));
sb.append(s.charAt(2));
sb.append(s.substring(26, 40));
sb.append(s.charAt(77));
sb.append(s.substring(41, 77));
sb.append(s.charAt(89));
sb.append(s.substring(78, 81));
} else if (s.length() == 89) {
sb.append(new StringBuilder(s.substring(79, 85)).reverse());
sb.append(s.charAt(87));
sb.append(new StringBuilder(s.substring(61, 78)).reverse());
sb.append(s.charAt(0));
sb.append(new StringBuilder(s.substring(4, 60)).reverse());
} else if (s.length() == 88) {
sb.append(s.substring(7, 28));
sb.append(s.charAt(87));
sb.append(s.substring(29, 45));
sb.append(s.charAt(55));
sb.append(s.substring(46, 55));
sb.append(s.charAt(2));
sb.append(s.substring(56, 87));
sb.append(s.charAt(28));
} else if (s.length() == 87) {
sb.append(s.substring(6, 27));
sb.append(s.charAt(4));
sb.append(s.substring(28, 39));
sb.append(s.charAt(27));
sb.append(s.substring(40, 59));
sb.append(s.charAt(2));
sb.append(s.substring(60));
} else if (s.length() == 86) {
sb.append(new StringBuilder(s.substring(73, 81)).reverse());
sb.append(s.charAt(16));
sb.append(new StringBuilder(s.substring(40, 72)).reverse());
sb.append(s.charAt(72));
sb.append(new StringBuilder(s.substring(17, 39)).reverse());
sb.append(s.charAt(82));
sb.append(new StringBuilder(s.substring(0, 16)).reverse());
} else if (s.length() == 85) {
sb.append(s.substring(3, 11));
sb.append(s.charAt(0));
sb.append(s.substring(12, 55));
sb.append(s.charAt(84));
sb.append(s.substring(56, 84));
} else if (s.length() == 84) {
sb.append(new StringBuilder(s.substring(71, 79)).reverse());
sb.append(s.charAt(14));
sb.append(new StringBuilder(s.substring(38, 70)).reverse());
sb.append(s.charAt(70));
sb.append(new StringBuilder(s.substring(15, 37)).reverse());
sb.append(s.charAt(80));
sb.append(new StringBuilder(s.substring(0, 13)).reverse());
} else if (s.length() == 83) {
sb.append(new StringBuilder(s.substring(64, 81)).reverse());
sb.append(s.charAt(0));
sb.append(new StringBuilder(s.substring(1, 63)).reverse());
sb.append(s.charAt(63));
} else if (s.length() == 82) {
sb.append(new StringBuilder(s.substring(38, 81)).reverse());
sb.append(s.charAt(7));
sb.append(new StringBuilder(s.substring(8, 37)).reverse());
sb.append(s.charAt(0));
sb.append(new StringBuilder(s.substring(1, 7)).reverse());
sb.append(s.charAt(37));
} else if (s.length() == 81) {
sb.append(s.charAt(56));
sb.append(new StringBuilder(s.substring(57, 80)).reverse());
sb.append(s.charAt(41));
sb.append(new StringBuilder(s.substring(42, 56)).reverse());
sb.append(s.charAt(80));
sb.append(new StringBuilder(s.substring(35, 41)).reverse());
sb.append(s.charAt(0));
sb.append(new StringBuilder(s.substring(30, 34)).reverse());
sb.append(s.charAt(34));
sb.append(new StringBuilder(s.substring(10, 29)).reverse());
sb.append(s.charAt(29));
sb.append(new StringBuilder(s.substring(1, 9)).reverse());
sb.append(s.charAt(9));
} else if (s.length() == 80) {
sb.append(s.substring(1, 19));
sb.append(s.charAt(0));
sb.append(s.substring(20, 68));
sb.append(s.charAt(19));
sb.append(s.substring(69, 80));
} else if (s.length() == 79) {
sb.append(s.charAt(54));
sb.append(new StringBuilder(s.substring(55, 78)).reverse());
sb.append(s.charAt(39));
sb.append(new StringBuilder(s.substring(40, 54)).reverse());
sb.append(s.charAt(78));
sb.append(new StringBuilder(s.substring(35, 39)).reverse());
sb.append(s.charAt(0));
sb.append(new StringBuilder(s.substring(30, 34)).reverse());
sb.append(s.charAt(34));
sb.append(new StringBuilder(s.substring(10, 29)).reverse());
sb.append(s.charAt(29));
sb.append(new StringBuilder(s.substring(1, 9)).reverse());
sb.append(s.charAt(9));
} else {
log("Unsupported SigLength: " + s.length());
return null;
}
return sb.toString();
}
private YouTubeSig getYouTubeSig(String html5player) {
// concurrency issues are not important in this point
if (YT_SIG == null) {
try {
HttpClient httpClient = HttpClientFactory.newDefaultInstance();
String jscode = httpClient.get(html5player.replace("\\", ""));
YT_SIG = new YouTubeSig(jscode);
} catch (Throwable t) {
LOG.error("Could not getYouTubeSig", t);
}
}
return YT_SIG;
}
private ThumbnailLinks createThumbnailLink(String videoId) {
String normal = "http://img.youtube.com/vi/" + videoId + "/default.jpg";
String mq = "http://img.youtube.com/vi/" + videoId + "/mqdefault.jpg";
String hq = "http://img.youtube.com/vi/" + videoId + "/hqdefault.jpg";
String maxres = "http://img.youtube.com/vi/" + videoId + "/maxresdefault.jpg";
return new ThumbnailLinks(normal, mq, hq, maxres);
}
private String unescape(final String s) {
char ch;
char ch2;
final StringBuilder sb = new StringBuilder();
int ii;
int i;
for (i = 0; i < s.length(); i++) {
ch = s.charAt(i);
switch (ch) {
case '%':
case '\\':
ch2 = ch;
ch = s.charAt(++i);
StringBuilder sb2 = null;
switch (ch) {
case 'u':
/* unicode */
sb2 = new StringBuilder();
i++;
ii = i + 4;
for (; i < ii; i++) {
ch = s.charAt(i);
if (sb2.length() > 0 || ch != '0') {
sb2.append(ch);
}
}
i--;
sb.append((char) Long.parseLong(sb2.toString(), 16));
continue;
case 'x':
/* normal hex coding */
sb2 = new StringBuilder();
i++;
ii = i + 2;
for (; i < ii; i++) {
ch = s.charAt(i);
sb2.append(ch);
}
i--;
sb.append((char) Long.parseLong(sb2.toString(), 16));
continue;
default:
if (ch2 == '%') {
sb.append(ch2);
}
sb.append(ch);
continue;
}
}
sb.append(ch);
}
return sb.toString();
}
private String cleanupFilename(String filename) {
return filename.replaceAll("[\\\\/:*?\"<>|\\[\\];,]+", "_");
}
private void log(String message) {
LOG.info(message);
}
private static Map<Integer, Format> buildFormats() {
Map<Integer, Format> formats = new HashMap<Integer, Format>();
formats.put(5, new Format("flv", "H263", "MP3", "240p"));
formats.put(6, new Format("flv", "H263", "MP3", "270p"));
formats.put(17, new Format("3gp", "H264", "AAC", "144p"));
formats.put(18, new Format("mp4", "H264", "AAC", "360p"));
formats.put(22, new Format("mp4", "H264", "AAC", "720p"));
formats.put(34, new Format("flv", "H264", "AAC", "360p"));
formats.put(35, new Format("flv", "H264", "AAC", "480p"));
formats.put(36, new Format("3gp", "H264", "AAC", "240p"));
formats.put(37, new Format("mp4", "H264", "AAC", "1080p"));
formats.put(38, new Format("mp4", "H264", "AAC", "3072p"));
formats.put(43, new Format("webm", "VP8", "Vorbis", "360p"));
formats.put(44, new Format("webm", "VP8", "Vorbis", "480p"));
formats.put(45, new Format("webm", "VP8", "Vorbis", "720p"));
formats.put(46, new Format("webm", "VP8", "Vorbis", "1080p"));
formats.put(82, new Format("mp4", "H264", "AAC", "360p"));
formats.put(83, new Format("mp4", "H264", "AAC", "240p"));
formats.put(84, new Format("mp4", "H264", "AAC", "720p"));
formats.put(85, new Format("mp4", "H264", "AAC", "520p"));
formats.put(100, new Format("webm", "VP8", "Vorbis", "360p"));
formats.put(101, new Format("webm", "VP8", "Vorbis", "360p"));
formats.put(102, new Format("webm", "VP8", "Vorbis", "720p"));
// dash video
formats.put(133, new Format("m4v", "H264", "", "240p"));
formats.put(134, new Format("m4v", "H264", "", "360p"));
formats.put(135, new Format("m4v", "H264", "", "480p"));
formats.put(136, new Format("m4v", "H264", "", "720p"));
formats.put(137, new Format("m4v", "H264", "", "1080p"));
// dash audio
formats.put(139, new Format("m4a", "", "AAC", "48k"));
formats.put(140, new Format("m4a", "", "AAC", "128k"));
formats.put(141, new Format("m4a", "", "AAC", "256k"));
return formats;
}
public static final class LinkInfo {
private LinkInfo(String link, int fmt, String filename, long size, Date date, String videoId, String user, String channel, ThumbnailLinks thumbnails, Format format) {
this.link = link;
this.fmt = fmt;
this.filename = filename;
this.size = size;
this.date = date;
this.videoId = videoId;
this.user = user;
this.channel = channel;
this.thumbnails = thumbnails;
this.format = format;
}
public final String link;
public final int fmt;
public final String filename;
public final long size;
public final Date date;
public final String videoId;
public final String user;
public final String channel;
public final ThumbnailLinks thumbnails;
public final Format format;
}
public static final class ThumbnailLinks {
private ThumbnailLinks(String normal, String mq, String hq, String maxres) {
this.normal = normal;
this.mq = mq;
this.hq = hq;
this.maxres = maxres;
}
public final String normal;
public final String mq;
public final String hq;
public final String maxres;
}
public static final class Format {
private Format(String ext, String video, String audio, String quality) {
this.ext = ext;
this.video = video;
this.audio = audio;
this.quality = quality;
}
public final String ext;
public final String video;
public final String audio;
public final String quality;
}
}