package com.flipstudio.youtube.extractor; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.util.SparseArray; import android.webkit.MimeTypeMap; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import javax.net.ssl.HttpsURLConnection; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; import static java.util.Arrays.asList; /** * Created by Pietro Caselani * On 06/03/14 * YouTubeExtractor */ public final class YouTubeExtractor { //region Fields public static final int YOUTUBE_VIDEO_QUALITY_SMALL_240 = 36; public static final int YOUTUBE_VIDEO_QUALITY_MEDIUM_360 = 18; public static final int YOUTUBE_VIDEO_QUALITY_HD_720 = 22; public static final int YOUTUBE_VIDEO_QUALITY_HD_1080 = 37; private final String mVideoIdentifier; private final List<String> mElFields; private HttpsURLConnection mConnection; private List<Integer> mPreferredVideoQualities; private boolean mCancelled; //endregion //region Constructors public YouTubeExtractor(String videoIdentifier) { mVideoIdentifier = videoIdentifier; mElFields = new ArrayList<String>(asList("embedded", "detailpage", "vevo", "")); mPreferredVideoQualities = asList(YOUTUBE_VIDEO_QUALITY_MEDIUM_360, YOUTUBE_VIDEO_QUALITY_SMALL_240, YOUTUBE_VIDEO_QUALITY_HD_720, YOUTUBE_VIDEO_QUALITY_HD_1080); } //endregion //region Getters and Setters public List<Integer> getPreferredVideoQualities() { return mPreferredVideoQualities; } public void setPreferredVideoQualities(List<Integer> preferredVideoQualities) { mPreferredVideoQualities = preferredVideoQualities; } //endregion //region Public public void startExtracting(final YouTubeExtractorListener listener) { String elField = mElFields.get(0); mElFields.remove(0); if (elField.length() > 0) elField = "&el=" + elField; final String language = Locale.getDefault().getLanguage(); final String link = String.format("https://www.youtube.com/get_video_info?video_id=%s%s&ps=default&eurl=&gl=US&hl=%s", mVideoIdentifier, elField, language); final HandlerThread youtubeExtractorThread = new HandlerThread("YouTubeExtractorThread", THREAD_PRIORITY_BACKGROUND); youtubeExtractorThread.start(); final Handler youtubeExtractorHandler = new Handler(youtubeExtractorThread.getLooper()); final Handler listenerHandler = new Handler(Looper.getMainLooper()); youtubeExtractorHandler.post(new Runnable() { @Override public void run() { try { mConnection = (HttpsURLConnection) new URL(link).openConnection(); mConnection.setRequestProperty("Accept-Language", language); BufferedReader reader = new BufferedReader(new InputStreamReader(mConnection.getInputStream())); StringBuilder builder = new StringBuilder(); String line; while ((line = reader.readLine()) != null && !mCancelled) builder.append(line); reader.close(); if (!mCancelled) { final YouTubeExtractorResult result = getYouTubeResult(builder.toString()); listenerHandler.post(new Runnable() { @Override public void run() { if (!mCancelled && listener != null) { listener.onSuccess(result); } } }); } } catch (final Exception e) { listenerHandler.post(new Runnable() { @Override public void run() { if (!mCancelled && listener != null) { listener.onFailure(new Error(e)); } } }); } finally { if (mConnection != null) { mConnection.disconnect(); } youtubeExtractorThread.quit(); } } }); } public void cancelExtracting() { mCancelled = true; } //endregion //region Private private static HashMap<String, String> getQueryMap(String queryString, String charsetName) throws UnsupportedEncodingException { HashMap<String, String> map = new HashMap<String, String>(); String[] fields = queryString.split("&"); for (String field : fields) { String[] pair = field.split("="); if (pair.length == 2) { String key = pair[0]; String value = URLDecoder.decode(pair[1], charsetName).replace('+', ' '); map.put(key, value); } } return map; } private YouTubeExtractorResult getYouTubeResult(String html) throws UnsupportedEncodingException, YouTubeExtractorException { HashMap<String, String> video = getQueryMap(html, "UTF-8"); Uri videoUri = null; if (video.containsKey("url_encoded_fmt_stream_map")) { List<String> streamQueries = new ArrayList<String>(asList(video.get("url_encoded_fmt_stream_map").split(","))); String adaptiveFmts = video.get("adaptive_fmts"); String[] split = adaptiveFmts.split(","); streamQueries.addAll(asList(split)); SparseArray<String> streamLinks = new SparseArray<String>(); for (String streamQuery : streamQueries) { HashMap<String, String> stream = getQueryMap(streamQuery, "UTF-8"); String type = stream.get("type").split(";")[0]; String urlString = stream.get("url"); if (urlString != null && MimeTypeMap.getSingleton().hasMimeType(type)) { String signature = stream.get("sig"); if (signature != null) { urlString = urlString + "&signature=" + signature; } if (getQueryMap(urlString, "UTF-8").containsKey("signature")) { streamLinks.put(Integer.parseInt(stream.get("itag")), urlString); } } } for (Integer videoQuality : mPreferredVideoQualities) { if (streamLinks.get(videoQuality, null) != null) { String streamLink = streamLinks.get(videoQuality); videoUri = Uri.parse(streamLink); break; } } final Uri mediumThumbUri = video.containsKey("iurlmq") ? Uri.parse(video.get("iurlmq")) : null; final Uri highThumbUri = video.containsKey("iurlhq") ? Uri.parse(video.get("iurlhq")) : null; final Uri defaultThumbUri = video.containsKey("iurl") ? Uri.parse(video.get("iurl")) : null; final Uri standardThumbUri = video.containsKey("iurlsd") ? Uri.parse(video.get("iurlsd")) : null; return new YouTubeExtractorResult(videoUri, mediumThumbUri, highThumbUri, defaultThumbUri, standardThumbUri); } else { throw new YouTubeExtractorException("Status: " + video.get("status") + "\nReason: " + video.get("reason") + "\nError code: " + video.get("errorcode")); } } //endregion public static final class YouTubeExtractorResult { private final Uri mVideoUri, mMediumThumbUri, mHighThumbUri; private final Uri mDefaultThumbUri, mStandardThumbUri; private YouTubeExtractorResult(Uri videoUri, Uri mediumThumbUri, Uri highThumbUri, Uri defaultThumbUri, Uri standardThumbUri) { mVideoUri = videoUri; mMediumThumbUri = mediumThumbUri; mHighThumbUri = highThumbUri; mDefaultThumbUri = defaultThumbUri; mStandardThumbUri = standardThumbUri; } public Uri getVideoUri() { return mVideoUri; } public Uri getMediumThumbUri() { return mMediumThumbUri; } public Uri getHighThumbUri() { return mHighThumbUri; } public Uri getDefaultThumbUri() { return mDefaultThumbUri; } public Uri getStandardThumbUri() { return mStandardThumbUri; } } public final class YouTubeExtractorException extends Exception { public YouTubeExtractorException(String detailMessage) { super(detailMessage); } } public interface YouTubeExtractorListener { void onSuccess(YouTubeExtractorResult result); void onFailure(Error error); } }